← back to blog
8 min read

TypeScript Tips for React Native Developers

Practical TypeScript patterns that will make your React Native code safer and more maintainable.

TypeScriptReact NativeBest Practices

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.