TypeScript Tips for React Native Developers
Practical TypeScript patterns that will make your React Native code safer and more maintainable.
TypeScript catches bugs before they reach users. Here are patterns I use in every React Native project.
1. Type Your Navigation
React Navigation with TypeScript prevents "screen doesn't exist" errors:
// navigation/types.ts
export type RootStackParamList = {
Home: undefined;
Profile: { userId: string };
Settings: undefined;
Post: { postId: string; title?: string };
};
// Now navigation is type-safe import { NativeStackNavigationProp } from '@react-navigation/native-stack';
type ProfileScreenProps = NativeStackNavigationProp< RootStackParamList, 'Profile' >;
function ProfileScreen({ navigation }: { navigation: ProfileScreenProps }) { // TypeScript knows this is valid navigation.navigate('Post', { postId: '123' });
// TypeScript error: missing required param 'userId' navigation.navigate('Profile', {}); }
2. API Response Types
Don't trust API responses. Validate and type them:
// types/api.ts
interface User {
id: string;
name: string;
email: string;
avatar?: string;
}
interface ApiResponse<T> { data: T; error?: string; }
// api/users.ts async function fetchUser(id: string): Promise<ApiResponse<User>> { const response = await fetch(/api/users/${id}); const json = await response.json();
// Runtime validation (use zod or yup for complex cases) if (!json.id || !json.name || !json.email) { return { data: null as any, error: 'Invalid user data' }; }
return { data: json as User }; }
For complex validation, use Zod:
import { z } from 'zod';
const UserSchema = z.object({ id: z.string(), name: z.string(), email: z.string().email(), avatar: z.string().url().optional(), });
type User = z.infer<typeof UserSchema>;
function parseUser(data: unknown): User { return UserSchema.parse(data); // Throws if invalid }
3. Component Props
Be explicit about props:
interface ButtonProps {
title: string;
onPress: () => void;
variant?: 'primary' | 'secondary' | 'danger';
disabled?: boolean;
loading?: boolean;
}
function Button({ title, onPress, variant = 'primary', disabled = false, loading = false, }: ButtonProps) { // Component implementation }
For components that extend native ones:
import { TouchableOpacityProps } from 'react-native';
interface CustomButtonProps extends TouchableOpacityProps { title: string; variant?: 'primary' | 'secondary'; }
function CustomButton({ title, variant, ...rest }: CustomButtonProps) { return ( <TouchableOpacity {...rest}> <Text>{title}</Text> </TouchableOpacity> ); }
4. useReducer with Discriminated Unions
Complex state logic benefits from typed reducers:
type State = {
status: 'idle' | 'loading' | 'success' | 'error';
data: User[] | null;
error: string | null;
};
type Action = | { type: 'FETCH_START' } | { type: 'FETCH_SUCCESS'; payload: User[] } | { type: 'FETCH_ERROR'; payload: string };
function reducer(state: State, action: Action): State { switch (action.type) { case 'FETCH_START': return { ...state, status: 'loading', error: null }; case 'FETCH_SUCCESS': return { status: 'success', data: action.payload, error: null }; case 'FETCH_ERROR': return { status: 'error', data: null, error: action.payload }; } }
TypeScript ensures you handle all action types.
5. Utility Types
Use built-in utility types:
// Pick specific properties
type UserPreview = Pick<User, 'id' | 'name' | 'avatar'>;
// Make all properties optional type PartialUser = Partial<User>;
// Make all properties required type RequiredUser = Required<User>;
// Exclude properties type UserWithoutEmail = Omit<User, 'email'>;
// Extract function return type type FetchUserReturn = ReturnType<typeof fetchUser>;
6. Generic Components
For reusable components:
interface ListProps<T> {
items: T[];
renderItem: (item: T, index: number) => React.ReactNode;
keyExtractor: (item: T) => string;
}
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) { return ( <View> {items.map((item, index) => ( <View key={keyExtractor(item)}> {renderItem(item, index)} </View> ))} </View> ); }
// Usage - TypeScript infers T from items <List items={users} renderItem={(user) => <Text>{user.name}</Text>} keyExtractor={(user) => user.id} />
Strict Mode
Enable strict TypeScript in tsconfig.json:
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"noImplicitReturns": true
}
}
It's annoying at first but catches real bugs. Worth it.