Lesson 16

TypeScript and React Navigation

Throughout this course we've used JavaScript-style { navigation, route } props without types. This works, but you lose autocomplete and type checking on screen names and params. React Navigation v7 has strong TypeScript support — once you set it up, the compiler catches navigation bugs before they reach your users.

Defining a Param List

Start by defining a type that maps each screen name to its params:

type RootStackParamList = {
  Home: undefined;
  Details: { itemId: number; title: string };
  Profile: { userId: string };
  Settings: undefined;
};

undefined means the screen takes no params. Screens with params specify the exact shape.

Typing the Navigator

Pass the param list as a generic to createNativeStackNavigator:

import { createNativeStackNavigator } from '@react-navigation/native-stack';

const Stack = createNativeStackNavigator<RootStackParamList>();

Now Stack.Screen enforces that the name prop matches a key in RootStackParamList. If you type <Stack.Screen name="Detials" /> (misspelled), TypeScript catches it.

Typing Screen Props

Use NativeStackScreenProps to type a screen component's props:

import type { NativeStackScreenProps } from '@react-navigation/native-stack';

type DetailsProps = NativeStackScreenProps<RootStackParamList, 'Details'>;

function DetailsScreen({ route, navigation }: DetailsProps) {
  const { itemId, title } = route.params; // typed: { itemId: number; title: string }

  return (
    <View style={styles.container}>
      <Text>{title}</Text>
      <Text>ID: {itemId}</Text>
      <Button
        title="Go to Profile"
        onPress={() => navigation.navigate('Profile', { userId: 'abc' })}
      />
    </View>
  );
}

The navigation.navigate call is now type-checked. If you try navigate('Profile') without the required userId param, TypeScript errors. If you try navigate('NonExistent'), it errors too.

Typing useNavigation and useRoute

When using hooks instead of props, specify the types:

import { useNavigation, useRoute } from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import type { RouteProp } from '@react-navigation/native';

function DetailsScreen() {
  const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList, 'Details'>>();
  const route = useRoute<RouteProp<RootStackParamList, 'Details'>>();

  const { itemId } = route.params; // typed
  navigation.navigate('Home'); // typed
}

Global Type Registration

To avoid passing generics to every useNavigation call, register your param list globally:

declare global {
  namespace ReactNavigation {
    interface RootParamList extends RootStackParamList {}
  }
}

Add this in your types file or at the top of your App.tsx. Now useNavigation() returns a typed navigation object without any generic:

function AnyScreen() {
  const navigation = useNavigation();
  navigation.navigate('Details', { itemId: 1, title: 'Widget' }); // fully typed
}

Typing Tab and Drawer Navigators

The same pattern applies to other navigator types:

import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import type { BottomTabScreenProps } from '@react-navigation/bottom-tabs';

type TabParamList = {
  Home: undefined;
  Search: undefined;
  Profile: { userId: string };
};

const Tab = createBottomTabNavigator<TabParamList>();

type ProfileProps = BottomTabScreenProps<TabParamList, 'Profile'>;

For drawers, use createDrawerNavigator<ParamList>() and DrawerScreenProps.

Complete Example

import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
import { View, Text, Button, FlatList, TouchableOpacity, StyleSheet } from 'react-native';

type RootStackParamList = {
  Home: undefined;
  Details: { itemId: number; title: string };
};

declare global {
  namespace ReactNavigation {
    interface RootParamList extends RootStackParamList {}
  }
}

const Stack = createNativeStackNavigator<RootStackParamList>();

const ITEMS = [
  { id: 1, title: 'Learn React Native' },
  { id: 2, title: 'Learn Navigation' },
  { id: 3, title: 'Build an App' },
];

type HomeProps = NativeStackScreenProps<RootStackParamList, 'Home'>;

function HomeScreen({ navigation }: HomeProps) {
  return (
    <FlatList
      data={ITEMS}
      keyExtractor={(item) => String(item.id)}
      renderItem={({ item }) => (
        <TouchableOpacity
          style={styles.item}
          onPress={() => navigation.navigate('Details', { itemId: item.id, title: item.title })}
        >
          <Text style={styles.itemTitle}>{item.title}</Text>
        </TouchableOpacity>
      )}
    />
  );
}

type DetailsProps = NativeStackScreenProps<RootStackParamList, 'Details'>;

function DetailsScreen({ route }: DetailsProps) {
  const { itemId, title } = route.params;

  return (
    <View style={styles.container}>
      <Text style={styles.title}>{title}</Text>
      <Text>Item ID: {itemId}</Text>
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, alignItems: 'center', justifyContent: 'center' },
  title: { fontSize: 24, marginBottom: 8 },
  item: { padding: 16, borderBottomWidth: 1, borderBottomColor: '#eee' },
  itemTitle: { fontSize: 18 },
});

export default function App() {
  return (
    <NavigationContainer>
      <Stack.Navigator>
        <Stack.Screen name="Home" component={HomeScreen} options={{ title: 'Items' }} />
        <Stack.Screen
          name="Details"
          component={DetailsScreen}
          options={({ route }) => ({ title: route.params.title })}
        />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

Every navigate call, every route param access, and every screen name is type-checked. Misspell a screen name or forget a required param and TypeScript tells you before you run the app.

In the next lesson — the capstone — we'll build a complete multi-navigator app that uses everything we've learned.