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.