Skip to main content

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 interface for object types, type for unions/intersections
  • Use unknown instead of any when 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