How to properly type useNavigation in React Navigation?

Viewed 9697

I'm trying to type the useNavigation from React Navigation. I would like to be able to pass only the name of the route, but I get an error unless I also pass props for that route.

Following the documentation, I understand the implementation should look something like this:

import { StackNavigationProp } from '@react-navigation/stack';

type StackParamList = {
    Home: { foo: string, onBar: () => void }
    About: AboutProps
}

type NavigationProps = StackNavigationProp<StackParamList>

const MyComponent = () => {
  const navigation = useNavigation<NavigationProps>()

  const handleOnNavigate = () => navigation.navigate('Home')
  //                                                    ^ TypeError!

I am geting a TypeError on the last line there. If I add the props for that route, the error disappears, but I shouldn't have to do that.

navigation.navigate('Home', { foo: 'hello', onBar: () => {} })

Looking at the type declaration for navigation.navigate (see below), this should not be necessary, as there is an overload for simply passing the name of the route as the only argument. I must be doing something wrong, since that is not accepted ... but what, where and why?

Here is a CodeSandBox reproducing the TypeError.

React Navigation types.d.ts (link)

navigate<RouteName extends keyof ParamList>(...args: undefined extends ParamList[RouteName]
  ? [screen: RouteName] | [screen: RouteName, params: ParamList[RouteName]]
  : [screen: RouteName, params: ParamList[RouteName]])
: void;
3 Answers

I am geting a TypeError on the last line there. If I add the props for that route, the error disappears, but I shouldn't have to do that.

You specified this in types:

Home: { foo: string, onBar: () => void }

Which means Home takes these params. If your route doesn't take any params and you could just do navigate('Home'), you shouldn't be specifying any params in the types.

If those params are optional, then you need to specify the type accordingly:

Home: { foo: string, onBar: () => void } | undefined

Within your type StackParamList you define what kind of params you expect when navigating to your screens. If you don't expect any params for Home, you also have to define it in your type accordingly:

export type StackParamList = {
  Home: undefined,
  About: { bar: string; onBaz: () => void };
};

If you expect any additional params optionally, you can use the pipe in your type declaration:

export type StackParamList = {
  Home: { foo: string; onBar: () => void } | undefined;
  About: { bar: string; onBaz: () => void };
};

// Will work in both ways: 

const handleOnNavigate = () => navigation.navigate('Home')

As a workaround you can also use another overload and pass a configuration object:

const handleOnNavigate = () => navigation.navigate({key : "Home"});

In order to make it work with only one argument you need to add | undefined.

Let me explain why you need to do it.

Consider this declareation:

navigate<RouteName extends keyof ParamList>(
    ...args: undefined extends ParamList[RouteName]
      ? [screen: RouteName] | [screen: RouteName, params: ParamList[RouteName]]
      : [screen: RouteName, params: ParamList[RouteName]]
  ): void;

This is the most interesting part: undefined extends ParamList[RouteName] It means that if undefined extends ParamList[RouteName] you are allowed to use only one argument.

Let's split it into smaller examples:

type ParamList = {
  Home: { foo: string; onBar: () => void };
  About: { bar: string; onBaz: () => void };
  Undefined: undefined,
};

type Check<T extends keyof ParamList> = undefined extends ParamList[T] ? 'might be undefined' : 'can not be undefined'

type Result = Check<'Home'> // "can not be undefined"
type Result2 = Check<'Undefined'> // "might be undefined"

As you might have noticed, if you provide Home TS will require two arguments because ParamList['Home'] returns an object which can not be undefined.

On the other hand, undefined extends ParamList['Undefined'] - hence TS allows you to use only one argument.

That's why TS does not allow you to pass only one argument.

Related