In a large React Navigation 7 TypeScript app, how do you type route params so navigate, useNavigation and useRoute are all checked?
answer
- one param list per navigator
- a type alias, not an interface
- global RootParamList declaration
- StaticParamList infers from config
- useRoute's generic is only a cast
basics
~20 sDeclare a param list type mapping each screen to its params, type screens with the navigator's ScreenProps helper, register the root list through the global ReactNavigation.RootParamList interface, and with static config infer the list via StaticParamList.
solid answer
~40 sEach navigator gets a param list, a `type` alias such as `{ Orders: undefined; Tracking: { orderId: string } }`; it must be a type alias because React Navigation constrains it to `ParamListBase`, an index-signature record that interfaces do not satisfy. Screens take `NativeStackScreenProps<RootStackParamList, 'Tracking'>`, so `route.params.orderId` and every `navigation.navigate` call are checked. To make argument-less `useNavigation()` and container refs know the routes, augment the global `ReactNavigation.RootParamList` interface with the root list. With the static API you skip writing the list: `StaticParamList<typeof RootStack>` infers it from the config and screens declare their params with `StaticScreenProps`. Two traps remain: the default `useNavigation()` type has only the helpers common to all navigators, so stack methods need `NativeStackNavigationProp`, and `useRoute<RouteProp<…>>()` is an unchecked cast.
code
tsx · 43 linesimport {
createStaticNavigation,
useNavigation,
type StaticParamList,
type StaticScreenProps,
} from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { Button, Text } from 'react-native';
function OrdersScreen() {
const navigation = useNavigation();
return (
<Button
title="Track order A123"
onPress={() => navigation.navigate('Tracking', { orderId: 'A123' })}
/>
);
}
function TrackingScreen({ route }: StaticScreenProps<{ orderId: string }>) {
return <Text>Tracking order {route.params.orderId}</Text>;
}
const RootStack = createNativeStackNavigator({
screens: {
Orders: OrdersScreen,
Tracking: TrackingScreen,
},
});
type RootStackParamList = StaticParamList<typeof RootStack>;
declare global {
namespace ReactNavigation {
interface RootParamList extends RootStackParamList {}
}
}
const Navigation = createStaticNavigation(RootStack);
export default function App() {
return <Navigation />;
}go deeper
Recall that a param list maps each screen name to its params and that a screen's props are typed from it with the navigator's ScreenProps helper.
Explain why the list must be a type alias, what the global RootParamList declaration adds for hooks, and how StaticParamList infers the list from static config.
Show the traps you guard against in a large codebase: the useRoute cast, the default useNavigation type lacking stack methods, and any or ParamListBase disabling checks downstream.
Treat param lists as versioned screen contracts owned by feature teams, with static config or colocated lists chosen to keep types and runtime from drifting apart.
## The param list is the contract In **React Navigation 7** with TypeScript, each navigator is typed by a **param list**: an object type whose keys are the screen names and whose values are that screen's params. - `Orders: undefined` means the screen takes no params. - `Tracking: { orderId: string }` means `orderId` is required. - `Search: { query?: string } | undefined` means params are optional altogether. The list has to be declared with **`type`**, not `interface`. React Navigation constrains param lists to `ParamListBase`, which is `Record<string, object | undefined>`; an object type alias is assignable to that index signature, while an interface is not, because interfaces do not get an implicit index signature. In a large app this type is the public contract of every screen. Renaming `orderId` to `id` becomes a compiler-guided change instead of a runtime crash on some rarely used path. ## Typing screens with the dynamic API With the dynamic API (`<Stack.Navigator>` and `<Stack.Screen>` elements), you pass the list to the factory and type each screen with the navigator's helper: ```tsx import { createNativeStackNavigator, type NativeStackScreenProps } from '@react-navigation/native-stack'; import { Text } from 'react-native'; type RootStackParamList = { Orders: undefined; Tracking: { orderId: string } }; const Stack = createNativeStackNavigator<RootStackParamList>(); type TrackingProps = NativeStackScreenProps<RootStackParamList, 'Tracking'>; export function TrackingScreen({ route }: TrackingProps) { return <Text>{route.params.orderId}</Text>; } ``` `NativeStackScreenProps` gives the screen a `navigation` typed as `NativeStackNavigationProp`, which includes the stack-only `push`, `replace` and `popTo`, and a `route` typed as `RouteProp` for that screen. Calling `navigation.navigate('Tracking')` without params, or with `{ id: 'A123' }`, is now a compile error. ## Typing the hooks and the root `useNavigation()` and `useRoute()` read from context, so TypeScript cannot know which screen they sit in. Three rules cover them: 1. **Register the root list globally.** Declaring `declare global { namespace ReactNavigation { interface RootParamList extends RootStackParamList {} } }` makes argument-less `useNavigation()`, container refs and the `Link` component type-check screen names and params against your routes. 2. **Ask for navigator-specific methods explicitly.** The default type of `useNavigation()` is a generic `NavigationProp` over the root list: `navigate`, `goBack`, `setParams` and the other helpers common to all navigators, but not `push` or `popTo`. For those, annotate: `useNavigation<NativeStackNavigationProp<RootStackParamList>>()`. 3. **Treat `useRoute`'s type argument as a cast.** `useRoute<RouteProp<RootStackParamList, 'Tracking'>>()` returns whatever route is in context as that type. If the component is later reused under another screen, `route.params.orderId` can be `undefined` at runtime while the compiler is satisfied. Prefer reading params in the screen and passing values down, or check `route.name` before trusting the params. ## Letting static config infer the list The static API (`createNativeStackNavigator({ screens: { … } })` rendered with `createStaticNavigation`) removes the hand-written list. Each screen declares its own params with `StaticScreenProps<{ orderId: string }>`, and `type RootStackParamList = StaticParamList<typeof RootStack>` infers the full list from the configuration, including screens that take none. The same global `RootParamList` declaration then types every hook in the app. | Aspect | Dynamic API | Static API | |---|---|---| | Where params are declared | a hand-written param list | on each screen via `StaticScreenProps` | | How the list is obtained | written and passed to the factory | inferred with `StaticParamList<typeof RootStack>` | | Global registration | `RootParamList extends RootStackParamList` | the same declaration | | Risk of drift | list and screens can disagree | the config is the only source | ## Keeping it maintainable at scale - Keep one param list per navigator, next to the navigator, and export it for the screens that use it. - Keep params minimal and serializable in the type itself: ids and plain values, never functions or class instances, so the types forbid what the runtime warns about. - Avoid `any` or `ParamListBase` in screen props; each one silently switches off checking for every call site downstream. - Type shared navigation helpers against the list too, for example `function openTracking(navigation: NativeStackNavigationProp<RootStackParamList>, orderId: string)`, so a helper cannot become the one untyped path into a screen. - Use `keyof RootStackParamList` where code handles route names generically, such as analytics on screen changes, so a renamed screen breaks the build instead of a dashboard. - Nested navigators need their own lists linked through the parent's params type, and URL-derived params arrive as strings until parsed, so the type describes the value after the linking config has converted it.
- Why does useNavigation() without a type argument not offer push or popTo?Its default type is a generic `NavigationProp` over the global `RootParamList`, which carries only the helpers every navigator shares, such as `navigate`, `goBack` and `setParams`. Stack-only methods need `useNavigation<NativeStackNavigationProp<RootStackParamList>>()`, or the screen's own `navigation` prop typed with `NativeStackScreenProps`.
- Does useRoute<RouteProp<RootStackParamList, 'Tracking'>>() prove the component is rendered inside Tracking?No. `useRoute` returns the route from context cast to the type you supply; nothing checks it. If the component is reused under another screen, `route.params.orderId` can be `undefined` at runtime with no compile error. Read params in the screen and pass values down, or check `route.name` first.
- Why must the param list be a type alias rather than an interface?React Navigation constrains param lists to `ParamListBase`, a `Record<string, object | undefined>`. An object type alias is assignable to that index signature; an interface is not, because interfaces get no implicit index signature, so an interface-based list fails the constraint.
saying these in an interview costs you the question
- Declaring the param list as an interface works the same as a type alias.
- useRoute's type argument guarantees the component sits inside that screen.
- Plain useNavigation() already knows push and popTo on a stack screen.
- Typing screen props as ParamListBase is fine, since it still accepts every route.
- Static configuration apps cannot type params, so they fall back to any.