Type-Safe React Navigation: Catch Route Typos Before Runtime
Move React Navigation route and parameter mistakes from runtime to compile time with a TypeScript ParamList.
14 Mar 2026, 07:16 UTC

A blank Details screen is a runtime navigation bug
You tap a list item, the app pushes a Details screen, and the screen renders nothing. The culprit is often a navigation parameter that arrived in the wrong shape: the screen expects itemId as a string, but the caller sent a number, or omitted it entirely. In an untyped React Navigation setup, route names are just strings and parameters are untyped objects, so these mistakes compile cleanly and fail only when a user hits the broken path.
React Navigation's TypeScript integration moves that failure earlier. By declaring a single ParamList type that maps every route name to its expected parameters, navigation.navigate() calls are checked against that contract. A typo in a route name or a mismatched parameter becomes a TypeScript error instead of a production crash.
Declare the ParamList once
A ParamList is a TypeScript interface where each key is a route name and each value is either the parameter object for that route or undefined when the route takes no parameters. Pass that type to your stack navigator factory so the navigator, its screens, and its navigation helpers inherit the same contract.
Assuming React Navigation v6 or later with @react-navigation/native-stack, the declaration looks like this:
import { createStackNavigator } from '@react-navigation/native-stack';
export type RootParamList = {
Home: undefined;
Details: { itemId: string };
};
const Stack = createStackNavigator<RootParamList>();
Exact helper names differ between React Navigation v6 and v7, so verify the generic and screen-props types against the docs for your installed major version.
Typed navigation in practice
With the stack typed, the compiler checks every navigation call. In this example, Details requires itemId as a string:
// Error: Argument of type '{ itemId: number; }' is not assignable to
// parameter of type '{ itemId: string; }'.
navigation.navigate('Details', { itemId: 42 });
// Error: Property 'itemId' is missing in type '{}' but required in
// '{ itemId: string; }'.
navigation.navigate('Details');
// OK
navigation.navigate('Details', { itemId: 'receipt-99' });
The same typing applies inside screen components. A Details screen can declare its props from the navigator type, so route.params is already known to be { itemId: string }:
import type { StackScreenProps } from '@react-navigation/native-stack';
type DetailsScreenProps = StackScreenProps<RootParamList, 'Details'>;
export const DetailsScreen = ({ route, navigation }: DetailsScreenProps) => {
const { itemId } = route.params; // string, no cast required
return <Text>Item {itemId}</Text>;
};
Helper names such as StackScreenProps are version-sensitive; check your installed React Navigation version before copying the snippet.
Limits: the type list is only as honest as you keep it
Type safety is compile-time only. If your ParamList says Details accepts no parameters but the screen reads route.params.itemId, TypeScript believes you and stays silent. The same erosion happens when nested navigators each maintain their own parameter contracts and the root list is not updated. React Navigation's global RootParamList augmentation pattern also assumes a single root navigator; apps with several top-level navigators are usually safer with explicit per-navigator generics.
Verify the setup
Run the TypeScript compiler from your project root:
npx tsc --noEmit
This command needs no special permissions and performs a read-only type check. A clean exit means your navigation calls match the declared ParamList. To confirm the net is actually catching errors, temporarily change Details in RootParamList to { itemId: number } while leaving the caller and screen unchanged, then run the command again. You should see type errors; revert the change afterward.
The practical takeaway is to define the ParamList on day one, before screens and navigation calls multiply. It is a small, explicit contract that turns common routing mistakes into compile-time failures instead of runtime blank screens.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.