Skip to main content
Version: 8.x

Type checking with TypeScript

React Navigation can be configured to type-check screens and their params, as well as various other APIs using TypeScript. This provides better intelliSense and type safety when working with React Navigation.

First, make sure you have the following configuration in your tsconfig.json under compilerOptions:

  • strict: true or strictNullChecks: true - Necessary for intelliSense and type inference to work correctly.
  • moduleResolution: "bundler" - Necessary to resolve the types correctly and match the behavior of Metro and other bundlers.

Setting up the types

There are 2 steps to configure TypeScript with the static API:

Specify the root navigator's type

For the type-inference to work, React Navigation needs to know the type of the root navigator in your app. To do this, you can declare a module augmentation for @react-navigation/core and extend the RootNavigator interface with the type of your root navigator.

const HomeTabs = createBottomTabNavigator({
screens: {
Feed: FeedScreen,
Profile: ProfileScreen,
},
});

const RootStack = createNativeStackNavigator({
screens: {
Home: HomeTabs,
},
});

type RootStackType = typeof RootStack;

declare module '@react-navigation/core' {
interface RootNavigator extends RootStackType {}
}

This is needed to type-check hooks such as useNavigation, useRoute, useNavigationState etc.

Specify param types for screens

After setting up the type for the root navigator, all we need to do is specify the type of params that our screens accept.

This can be done in 2 ways:

  1. The type annotation for the component specified in screen:

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

    type ProfileParams = {
    userId: string;
    };

    function ProfileScreen({ route }: StaticScreenProps<ProfileParams>) {
    // ...
    }

    In the above example, the type of route.params is { userId: string } based on the type annotation in StaticScreenProps<ProfileParams>.

    If you aren't using the route object in the component, you can specify the props as _ to avoid unused variable warnings:

    function ProfileScreen(_: StaticScreenProps<ProfileParams>) {
    // ...
    }
  2. The path pattern specified in the linking config (e.g. for linking: 'profile/:userId', the type of route.params is { userId: string }). The type can be further customized by using a parse function in the linking config:

    linking: {
    path: 'profile/:userId',
    parse: {
    userId: (id) => parseInt(id, 10),
    },
    },

    The above example would make the type of route.params be { userId: number } since the parse function converts the string from the URL to a number.

If both screen and linking specify params, the final type of route.params is the intersection of both types.

This is how the complete example would look like:

const MyStack = createNativeStackNavigator({
screens: {
Profile: createNativeStackScreen({
screen: ProfileScreen,
linking: {
path: 'profile/:userId',
parse: {
userId: (id) => parseInt(id, 10),
},
},
}),
},
});

If your app supports deep linking or runs on the Web, it is recommended to specify params that appear in the path pattern in the linking config. Any additional params (e.g. query params) can be specified in the component's props.

If you have specified the params in linking, it's recommended to not specify them again in the component's props, and use useRoute('ScreenName') instead to get the correctly typed route object.

The createXScreen helper functions enable type inference in screen configuration callbacks like options, listeners, etc. Each navigator exports its own version of the helper function:

  • createNativeStackScreen from @react-navigation/native-stack
  • createStackScreen from @react-navigation/stack
  • createBottomTabScreen from @react-navigation/bottom-tabs
  • createDrawerScreen from @react-navigation/drawer
  • createMaterialTopTabScreen from @react-navigation/material-top-tabs

See Static configuration for more details.

Using typed hooks

The useRoute, useNavigation, and useNavigationState hooks accept the name of the current screen or any parent screen where it's nested as an argument to infer the correct types.

Once the types are set up, these hooks are automatically typed based on the name of the screen passed to them.

With useRoute:

function ProfileScreen() {
const route = useRoute('Profile');

// The params are correctly typed here
const { userId } = route.params;

// ...
}

With useNavigation:

function ProfileScreen() {
const navigation = useNavigation('Profile');

// Helpers like `push` are correctly typed here
navigation.push('Feed');

// ...
}

With useNavigationState:

function ProfileScreen() {
const focusedRouteName = useNavigationState(
'Profile',
// The state is correctly typed here
(state) => state.routes[state.index].name
);

// The `focusedRouteName` type is one of the route names
// defined in the navigator where `Profile` is defined
console.log(focusedRouteName);

// ...
}

It's also possible to use these hooks without specifying the screen name - which can be useful in re-usable components that can be used across multiple screens. In this case, different things happen based on the hook.

The useRoute hook returns a union of all routes in the app, and can be narrowed down using type guards:

function Header() {
const route = useRoute();

// The route is an union of all routes in the app
console.log(route.name);

// It's possible to narrow down the type using type guards
if (route.name === 'Profile') {
// Here route.params is correctly typed
const { userId } = route.params;
}

// ...
}

The useNavigation hook returns a generic navigation object that refers to the root navigator. This means that any navigation actions can be called as if they are used in a screen of the root navigator:

function Header() {
const navigation = useNavigation();

// A generic navigation object that refers to the root navigator
navigation.navigate('Profile', { userId: '123' });

// ...
}

The useNavigationState hook returns a generic navigation state without any navigator-specific types:

function Header() {
const focusedRouteName = useNavigationState((state) => {
// The state is a generic navigation state
return state.routes[state.index].name;
});

// The `focusedRouteName` type is `string`
console.log(focusedRouteName);

// ...
}

Nesting navigator using dynamic API

Consider the following example:

const Tab = createBottomTabNavigator();

function HomeTabs() {
return (
<Tab.Navigator>
<Tab.Screen name="Feed" component={FeedScreen} />
<Tab.Screen name="Profile" component={ProfileScreen} />
</Tab.Navigator>
);
}

const RootStack = createStackNavigator({
screens: {
Home: HomeTabs,
},
});

Here, the HomeTabs component is defined using the dynamic API. This means that React Navigation won't know about the screens defined in the nested navigator and the types for those screens. To fix this, we'd need to specify the types for the nested navigator explicitly.

This can be done by annotating the type of the route prop that the screen component receives:

type HomeTabsParamList = {
Feed: undefined;
Profile: undefined;
};

type HomeTabsProps = StaticScreenProps<
NavigatorScreenParams<HomeTabsParamList>
>;

function HomeTabs(_: HomeTabsProps) {
return (
<Tab.Navigator>
<Tab.Screen name="Feed" component={FeedScreen} />
<Tab.Screen name="Profile" component={ProfileScreen} />
</Tab.Navigator>
);
}

Here the HomeTabsParamList type defines the mapping of route names in the tab navigator to the types of their params. We then use the NavigatorScreenParams utility to say that these are the screens in a nested navigator in the HomeTabs component.

Now, React Navigation knows about the screens in the nested navigator and their params, and the types can be inferred with hooks such as useRoute.