Appendix B
📘 TypeScript Cheat Sheet
Essential TypeScript patterns for React Native development
Component Types
Basic Function Component
import { View, Text } from 'react-native';
// Simple component without props
function Welcome(): JSX.Element {
return <Text>Welcome!</Text>;
}
// With React.FC (includes children by default)
const Welcome: React.FC = () => {
return <Text>Welcome!</Text>;
};
// Recommended: explicit return type
function Welcome(): React.ReactElement {
return <Text>Welcome!</Text>;
}
Component with Props
// Define props interface
interface GreetingProps {
name: string;
age?: number; // Optional prop
}
// Function component with props
function Greeting({ name, age }: GreetingProps): JSX.Element {
return (
<View>
<Text>Hello, {name}!</Text>
{age && <Text>Age: {age}</Text>}
</View>
);
}
// Arrow function variant
const Greeting = ({ name, age }: GreetingProps): JSX.Element => {
return <Text>Hello, {name}!</Text>;
};
Component with Children
import { PropsWithChildren } from 'react';
// Using PropsWithChildren helper
interface CardProps {
title: string;
}
function Card({ title, children }: PropsWithChildren<CardProps>): JSX.Element {
return (
<View>
<Text>{title}</Text>
{children}
</View>
);
}
// Explicit children type
interface CardProps {
title: string;
children: React.ReactNode;
}
// Specific children type
interface ListProps {
children: React.ReactElement | React.ReactElement[];
}
Generic Components
// Generic list component
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactElement;
keyExtractor: (item: T) => string;
}
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>): JSX.Element {
return (
<View>
{items.map(item => (
<View key={keyExtractor(item)}>
{renderItem(item)}
</View>
))}
</View>
);
}
// Usage
<List
items={users}
renderItem={(user) => <Text>{user.name}</Text>}
keyExtractor={(user) => user.id}
/>
Props Patterns
Default Props
interface ButtonProps {
title: string;
variant?: 'primary' | 'secondary';
disabled?: boolean;
}
// Default values in destructuring
function Button({
title,
variant = 'primary',
disabled = false
}: ButtonProps): JSX.Element {
return (
<Pressable disabled={disabled}>
<Text>{title}</Text>
</Pressable>
);
}
Extending Native Props
import { PressableProps, TextInputProps, ViewProps } from 'react-native';
// Extend Pressable props
interface CustomButtonProps extends PressableProps {
title: string;
variant?: 'primary' | 'secondary';
}
function CustomButton({ title, variant, ...pressableProps }: CustomButtonProps) {
return (
<Pressable {...pressableProps}>
<Text>{title}</Text>
</Pressable>
);
}
// Extend TextInput props
interface CustomInputProps extends TextInputProps {
label: string;
error?: string;
}
// Extend View props
interface CardProps extends ViewProps {
title: string;
}
Discriminated Unions (Conditional Props)
// Button can be 'button' type with onPress OR 'link' type with href
type ButtonProps =
| {
type: 'button';
onPress: () => void;
href?: never;
}
| {
type: 'link';
href: string;
onPress?: never;
};
interface BaseButtonProps {
title: string;
disabled?: boolean;
}
function Button({ title, ...props }: BaseButtonProps & ButtonProps) {
if (props.type === 'button') {
return <Pressable onPress={props.onPress}><Text>{title}</Text></Pressable>;
}
return <Link href={props.href}>{title}</Link>;
}
Callback Props
interface FormProps {
// Simple callback
onSubmit: () => void;
// Callback with parameter
onChange: (value: string) => void;
// Callback with multiple parameters
onError: (error: Error, context: string) => void;
// Async callback
onSave: () => Promise<void>;
// Optional callback
onCancel?: () => void;
// Callback returning value
validate: (value: string) => boolean;
}
Hooks Types
useState
// Type inferred from initial value
const [count, setCount] = useState(0); // number
const [name, setName] = useState(''); // string
const [active, setActive] = useState(false); // boolean
// Explicit type for complex values
interface User {
id: string;
name: string;
email: string;
}
const [user, setUser] = useState<User | null>(null);
const [users, setUsers] = useState<User[]>([]);
// Union types
const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
// Lazy initialization
const [state, setState] = useState<ExpensiveState>(() => computeExpensiveValue());
useRef
import { useRef } from 'react';
import { TextInput, View, ScrollView } from 'react-native';
// DOM/Native element refs
const inputRef = useRef<TextInput>(null);
const viewRef = useRef<View>(null);
const scrollRef = useRef<ScrollView>(null);
// Usage
inputRef.current?.focus();
scrollRef.current?.scrollToEnd();
// Mutable value ref (doesn't trigger re-render)
const countRef = useRef<number>(0);
countRef.current = 5;
// Store previous value
const prevValueRef = useRef<string>();
useEffect(() => {
prevValueRef.current = value;
}, [value]);
useReducer
// Define state and action types
interface State {
count: number;
error: string | null;
}
type Action =
| { type: 'increment' }
| { type: 'decrement' }
| { type: 'reset'; payload: number }
| { type: 'setError'; payload: string };
// Reducer function
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'increment':
return { ...state, count: state.count + 1 };
case 'decrement':
return { ...state, count: state.count - 1 };
case 'reset':
return { ...state, count: action.payload };
case 'setError':
return { ...state, error: action.payload };
default:
return state;
}
}
// Usage
const [state, dispatch] = useReducer(reducer, { count: 0, error: null });
dispatch({ type: 'increment' });
dispatch({ type: 'reset', payload: 10 });
useContext
import { createContext, useContext, PropsWithChildren } from 'react';
// Define context type
interface AuthContextType {
user: User | null;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
isLoading: boolean;
}
// Create context with default value
const AuthContext = createContext<AuthContextType | undefined>(undefined);
// Custom hook with type safety
function useAuth(): AuthContextType {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}
// Provider component
function AuthProvider({ children }: PropsWithChildren): JSX.Element {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(false);
const login = async (email: string, password: string) => { /* ... */ };
const logout = () => { /* ... */ };
return (
<AuthContext.Provider value={{ user, login, logout, isLoading }}>
{children}
</AuthContext.Provider>
);
}
Custom Hooks
// Return tuple
function useToggle(initial: boolean = false): [boolean, () => void] {
const [value, setValue] = useState(initial);
const toggle = useCallback(() => setValue(v => !v), []);
return [value, toggle];
}
// Return object
interface UseCounterReturn {
count: number;
increment: () => void;
decrement: () => void;
reset: () => void;
}
function useCounter(initial: number = 0): UseCounterReturn {
const [count, setCount] = useState(initial);
return {
count,
increment: () => setCount(c => c + 1),
decrement: () => setCount(c => c - 1),
reset: () => setCount(initial),
};
}
// Generic custom hook
function useAsync<T>(asyncFn: () => Promise<T>) {
const [data, setData] = useState<T | null>(null);
const [error, setError] = useState<Error | null>(null);
const [loading, setLoading] = useState(false);
// ... implementation
return { data, error, loading };
}
Style Types
StyleSheet Types
import { StyleSheet, ViewStyle, TextStyle, ImageStyle } from 'react-native';
// Individual style types
const containerStyle: ViewStyle = {
flex: 1,
backgroundColor: '#fff',
};
const textStyle: TextStyle = {
fontSize: 16,
fontWeight: 'bold',
};
const imageStyle: ImageStyle = {
width: 100,
height: 100,
resizeMode: 'cover',
};
// StyleSheet.create is typed automatically
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 16,
},
title: {
fontSize: 24,
fontWeight: 'bold',
},
});
Style Props
import { StyleProp, ViewStyle, TextStyle } from 'react-native';
interface CardProps {
// Single style
style?: ViewStyle;
// Style prop (allows arrays and undefined)
containerStyle?: StyleProp<ViewStyle>;
titleStyle?: StyleProp<TextStyle>;
}
function Card({ style, containerStyle, titleStyle }: CardProps) {
return (
<View style={[styles.container, containerStyle]}>
<Text style={[styles.title, titleStyle]}>Title</Text>
</View>
);
}
// Usage - all valid
<Card style={{ padding: 10 }} />
<Card containerStyle={[styles.custom, { margin: 5 }]} />
<Card containerStyle={isActive && styles.active} />
Dynamic Styles
// Function returning styles
const getDynamicStyles = (isActive: boolean): ViewStyle => ({
backgroundColor: isActive ? 'blue' : 'gray',
opacity: isActive ? 1 : 0.5,
});
// Typed style function
type VariantStyles = {
[key in 'primary' | 'secondary' | 'danger']: ViewStyle;
};
const variantStyles: VariantStyles = {
primary: { backgroundColor: '#007AFF' },
secondary: { backgroundColor: '#5856D6' },
danger: { backgroundColor: '#FF3B30' },
};
// Usage
const buttonStyle = variantStyles[variant];
Event Types
Common Event Types
import {
GestureResponderEvent,
NativeSyntheticEvent,
TextInputChangeEventData,
TextInputSubmitEditingEventData,
TextInputFocusEventData,
LayoutChangeEvent,
NativeScrollEvent,
} from 'react-native';
// Press events
const handlePress = (event: GestureResponderEvent) => {
console.log('Pressed at:', event.nativeEvent.locationX);
};
// TextInput events
const handleChange = (e: NativeSyntheticEvent<TextInputChangeEventData>) => {
console.log('New text:', e.nativeEvent.text);
};
const handleSubmit = (e: NativeSyntheticEvent<TextInputSubmitEditingEventData>) => {
console.log('Submitted:', e.nativeEvent.text);
};
// Simpler: just use the value
const handleChangeText = (text: string) => {
setValue(text);
};
// Layout events
const handleLayout = (event: LayoutChangeEvent) => {
const { width, height, x, y } = event.nativeEvent.layout;
};
// Scroll events
const handleScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
const { contentOffset, contentSize } = event.nativeEvent;
};
FlatList Event Types
import { ListRenderItem, ListRenderItemInfo } from 'react-native';
interface Item {
id: string;
title: string;
}
// renderItem function type
const renderItem: ListRenderItem<Item> = ({ item, index }) => (
<Text>{item.title}</Text>
);
// Or with full info object
const renderItem = ({ item, index, separators }: ListRenderItemInfo<Item>) => (
<Pressable
onPressIn={() => separators.highlight()}
onPressOut={() => separators.unhighlight()}
>
<Text>{item.title}</Text>
</Pressable>
);
// keyExtractor
const keyExtractor = (item: Item, index: number): string => item.id;
API and Data Types
API Response Types
// Define your data types
interface User {
id: string;
name: string;
email: string;
avatar?: string;
createdAt: string;
}
interface PaginatedResponse<T> {
data: T[];
page: number;
totalPages: number;
totalItems: number;
}
interface ApiError {
message: string;
code: string;
status: number;
}
// Typed fetch function
async function fetchUsers(): Promise<PaginatedResponse<User>> {
const response = await fetch('/api/users');
if (!response.ok) {
const error: ApiError = await response.json();
throw new Error(error.message);
}
return response.json();
}
// Generic fetch wrapper
async function api<T>(url: string, options?: RequestInit): Promise<T> {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json() as Promise<T>;
}
// Usage
const users = await api<User[]>('/api/users');
const user = await api<User>('/api/users/123');
Form Data Types
// Form state type
interface LoginForm {
email: string;
password: string;
rememberMe: boolean;
}
// Form errors type
type FormErrors<T> = Partial<Record<keyof T, string>>;
// Usage
const [form, setForm] = useState<LoginForm>({
email: '',
password: '',
rememberMe: false,
});
const [errors, setErrors] = useState<FormErrors<LoginForm>>({});
// Validation function
function validate(form: LoginForm): FormErrors<LoginForm> {
const errors: FormErrors<LoginForm> = {};
if (!form.email) errors.email = 'Email is required';
if (!form.password) errors.password = 'Password is required';
return errors;
}
AsyncStorage Types
import AsyncStorage from '@react-native-async-storage/async-storage';
// Typed storage helpers
async function storeData<T>(key: string, value: T): Promise<void> {
const jsonValue = JSON.stringify(value);
await AsyncStorage.setItem(key, jsonValue);
}
async function getData<T>(key: string): Promise<T | null> {
const jsonValue = await AsyncStorage.getItem(key);
return jsonValue != null ? JSON.parse(jsonValue) as T : null;
}
// Usage
interface Settings {
theme: 'light' | 'dark';
notifications: boolean;
}
await storeData<Settings>('settings', { theme: 'dark', notifications: true });
const settings = await getData<Settings>('settings');
Utility Types
Built-in Utility Types
interface User {
id: string;
name: string;
email: string;
age: number;
}
// Partial - all properties optional
type PartialUser = Partial<User>;
// { id?: string; name?: string; email?: string; age?: number; }
// Required - all properties required
type RequiredUser = Required<PartialUser>;
// Pick - select specific properties
type UserPreview = Pick<User, 'id' | 'name'>;
// { id: string; name: string; }
// Omit - exclude specific properties
type UserWithoutEmail = Omit<User, 'email'>;
// { id: string; name: string; age: number; }
// Record - create object type
type UserRoles = Record<string, 'admin' | 'user' | 'guest'>;
// { [key: string]: 'admin' | 'user' | 'guest' }
// Exclude - remove types from union
type Status = 'idle' | 'loading' | 'success' | 'error';
type ActiveStatus = Exclude<Status, 'idle'>;
// 'loading' | 'success' | 'error'
// Extract - keep only matching types
type SuccessStatus = Extract<Status, 'success' | 'error'>;
// 'success' | 'error'
// NonNullable - remove null and undefined
type MaybeString = string | null | undefined;
type DefiniteString = NonNullable<MaybeString>;
// string
Custom Utility Types
// Make specific properties optional
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
// Usage: User with optional email
type UserOptionalEmail = PartialBy<User, 'email'>;
// Make specific properties required
type RequiredBy<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;
// Deep partial (nested objects also partial)
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
// Nullable type
type Nullable<T> = T | null;
// Array element type
type ArrayElement<T> = T extends (infer E)[] ? E : never;
// Usage
type Users = User[];
type SingleUser = ArrayElement<Users>; // User
// Function return type
type FetchUserReturn = ReturnType<typeof fetchUser>;
// Function parameter types
type FetchUserParams = Parameters<typeof fetchUser>;
Type Guards
// Type guard function
function isUser(obj: unknown): obj is User {
return (
typeof obj === 'object' &&
obj !== null &&
'id' in obj &&
'name' in obj &&
'email' in obj
);
}
// Usage
function processData(data: unknown) {
if (isUser(data)) {
// TypeScript knows data is User here
console.log(data.email);
}
}
// Array type guard
function isUserArray(arr: unknown): arr is User[] {
return Array.isArray(arr) && arr.every(isUser);
}
// Discriminated union guard
type Result<T> =
| { success: true; data: T }
| { success: false; error: string };
function handleResult<T>(result: Result<T>) {
if (result.success) {
// TypeScript knows result.data exists
console.log(result.data);
} else {
// TypeScript knows result.error exists
console.error(result.error);
}
}
✅ TypeScript Best Practices
- Prefer
interfacefor object types,typefor unions/intersections - Use
unknowninstead ofanywhen type is truly unknown - Enable strict mode in tsconfig.json
- Use type inference when the type is obvious
- Create reusable types in a central
types/directory - Use type guards for runtime type checking
- Avoid type assertions (
as) when possible