Type‑Safe Navigation in React Navigation v6 with TypeScript
Learn how to define a RootStackParamList, get autocomplete for navigate() calls, and catch missing params at compile time.
23 Sept 2025, 06:53 UTC

The problem: untyped navigation leads to runtime bugs
When you call navigate('Profile', { userId: '123' }) in a React Navigation v6 app, TypeScript will happily accept any object as the second argument. If you forget userId or typo it as userid, the error only surfaces when the screen tries to read the param – often after a user has already navigated away. This makes refactoring risky and forces you to rely on manual testing or runtime guards.
Defining a typed RootStackParamList
The first step is to declare a TypeScript type that maps each route name to the shape of its parameters. This type lives alongside your navigator and is fed into the navigation prop generic.
// src/navigation/types.ts
import { StackParamList } from '@react-navigation/native-stack';
export type RootStackParamList = {
Home: undefined;
Profile: { userId: string };
Detail: { itemId: number; section?: 'summary' | 'metrics' };
// add more screens as needed
};
With this list in place, you can type the navigation prop:
import { NavigationProp } from '@react-navigation/native';
import type { RootStackParamList } from './types';
type RootNavProp = NavigationProp;
Worked example: typed navigate and useNavigation
Create a new React Native project with TypeScript template (you need Node ≥ 18 and watchman or similar for iOS/Android builds). Run the following in your terminal:
npx @react-native-community/cli init TypeSafeNav --template react-native-template-typescriptcd TypeSafeNavnpm install @react-navigation/native @react-navigation/native-stacknpx pod-install ios(if targeting iOS)
Now add the typed param list and a screen that uses it:
// src/navigation/AppNavigator.tsx
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import type { RootStackParamList } from './types';
import { HomeScreen } from '../screens/HomeScreen';
import { ProfileScreen } from '../screens/ProfileScreen';
const Stack = createNativeStackNavigator();
export default function AppNavigator() {
return (
);
}
The screen component can now request a typed navigation prop:
// src/screens/ProfileScreen.tsx
import React from 'react';
import { Button, View, Text } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import type { NavigationProp } from '@react-navigation/native';
import type { RootStackParamList } from '../navigation/types';
type ProfileNavProp = NavigationProp;
export const ProfileScreen = () => {
const nav = useNavigation();
const goToDetail = () => {
// ✅ correct param name and type
nav.navigate('Detail', { itemId: 42, section: 'metrics' });
// ❌ TypeScript error if you uncomment:
// nav.navigate('Detail', { itemId: '42' }); // number expected
// nav.navigate('Detail', { itemId: 42, sect: 'metrics' }); // typo
};
return (
Profile screen
);
};
To verify the type safety, run the TypeScript compiler:
npx tsc --noEmitIf the project compiles without errors, your navigate calls match the declared param shapes. Introduce a mistake – e.g., change
itemId: 42toitemId: '42'– and you’ll see a diagnostic like:Type 'string' is not assignable to type 'number'.Trade‑offs and limitations
- TypeScript version: Template‑literal route names (e.g.,
Profile/{userId}) are fully inferred only in TS 4.7+. Older versions fall back to a string index signature, losing some autocomplete precision. - Dynamic routes: If you add routes via code‑splitting or plugins at runtime, the static
RootStackParamListwon’t reflect them unless you manually merge declarations or generate types. - Large param lists: A monolithic list with dozens of routes can slow the language service. Consider splitting into feature‑specific lists and using declaration merging:
// src/navigation/homeTypes.ts
export type HomeStackParamList = { Home: undefined; Settings: { tab: string } };
// src/navigation/profileTypes.ts
export type ProfileStackParamList = { Profile: { userId: string }; Detail: { itemId: number } };
// src/navigation/types.ts
import type { HomeStackParamList } from './homeTypes';
import type { ProfileStackParamList } from './profileTypes';
export type RootStackParamList = HomeStackParamList & ProfileStackParamList;
Remember that TypeScript only checks navigation calls written in your source code. Deep links or external navigation (e.g., from a push notification) still need runtime validation – you can use a library like Zod to inspect the incoming params before handing them to a screen.
Actionable closing
- Add a
RootStackParamListfile to your project and give every screen its exact param shape. - Type your navigation prop with
NavigationPropand use the generic overload ofuseNavigation. - Run
npx tsc --noEmitas part of your CI pipeline to catch missing or misspelled params early. - If your app grows, split the param list by feature and merge them with TypeScript declaration merging to keep the language service responsive.
- For any navigation source that isn’t TypeScript‑checked (deep links, web‑to‑app redirects), validate the params at runtime before calling
navigate.
By following these steps, you turn navigation from a source of subtle runtime bugs into a compile‑time‑checked contract, giving you IDE autocomplete, safer refactors, and confidence that the params you pass are exactly what the destination screen expects.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.