Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: makes NavigationContainerRef.getCurrentRoute type safe #11525

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
41 changes: 41 additions & 0 deletions example/__typechecks__/common.check.tsx
Expand Up @@ -11,8 +11,10 @@ import type {
import type {
CompositeScreenProps,
NavigationAction,
NavigationContainerRef,
NavigationHelpers,
NavigatorScreenParams,
Route,
} from '@react-navigation/native';
import {
createStackNavigator,
Expand Down Expand Up @@ -498,3 +500,42 @@ const FourthStack = createStackNavigator<FourthParamList, 'MyID'>();
expectTypeOf(FourthStack.Navigator).parameter(0).toMatchTypeOf<{
id: 'MyID';
}>();

/**
* Check for errors on getCurrentRoute
*/
declare const navigationRef: NavigationContainerRef<RootStackParamList>;
const route = navigationRef.getCurrentRoute()!;

switch (route.name) {
case 'PostDetails':
expectTypeOf(route.params).toMatchTypeOf<{
id: string;
section?: string;
}>();
break;
case 'Login':
expectTypeOf(route.params).toMatchTypeOf<undefined>();
break;
case 'NotFound':
expectTypeOf(route.params).toMatchTypeOf<undefined>();
break;
// Checks for nested routes
case 'Account':
expectTypeOf(route.params).toMatchTypeOf<undefined>();
break;
case 'Popular':
expectTypeOf(route.params).toMatchTypeOf<{
filter: 'day' | 'week' | 'month';
}>();
break;
case 'Latest':
expectTypeOf(route.params).toMatchTypeOf<undefined>();
break;
}

declare const navigationRefUntyped: NavigationContainerRef<string>;

expectTypeOf(navigationRefUntyped.getCurrentRoute()).toMatchTypeOf<
Route<string> | undefined
>();
44 changes: 44 additions & 0 deletions example/__typechecks__/static.check.tsx
Expand Up @@ -2,6 +2,7 @@

import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import type {
NavigationContainerRef,
NavigationProp,
NavigatorScreenParams,
StaticParamList,
Expand Down Expand Up @@ -119,6 +120,7 @@ navigation.navigate('Register', { method: 'token' });
/**
* Infer params from nested navigator
*/
navigation.navigate('Home'); // Navigate to screen without specifying a child screen
navigation.navigate('Home', { screen: 'Groups' });
navigation.navigate('Home', { screen: 'Chat', params: { id: 123 } });

Expand Down Expand Up @@ -340,3 +342,45 @@ expectTypeOf<MyParamList>().toMatchTypeOf<{
}>
| undefined;
}>();

/**
* Check for errors on getCurrentRoute
*/
declare const navigationRef: NavigationContainerRef<RootParamList>;
const route = navigationRef.getCurrentRoute()!;

switch (route.name) {
case 'Profile':
expectTypeOf(route.params).toMatchTypeOf<{
user: string;
}>();
break;
case 'Feed':
expectTypeOf(route.params).toMatchTypeOf<{
sort: 'hot' | 'recent';
}>();
break;
case 'Settings':
expectTypeOf(route.params).toMatchTypeOf<undefined>();
break;
case 'Login':
expectTypeOf(route.params).toMatchTypeOf<undefined>();
break;
case 'Register':
expectTypeOf(route.params).toMatchTypeOf<{
method: 'email' | 'social';
}>();
break;
case 'Account':
expectTypeOf(route.params).toMatchTypeOf<undefined>();
break;
// Checks for nested routes
case 'Groups':
expectTypeOf(route.params).toMatchTypeOf<undefined>();
break;
case 'Chat':
expectTypeOf(route.params).toMatchTypeOf<{
id: number;
}>();
break;
}
16 changes: 15 additions & 1 deletion packages/core/src/types.tsx
Expand Up @@ -775,6 +775,20 @@ export type NavigationContainerEventMap = {
};
};

type NotUndefined<T> = T extends undefined ? never : T;

export type ParamListRoute<ParamList extends ParamListBase> = {
[RouteName in keyof ParamList]: NavigatorScreenParams<{}> extends ParamList[RouteName]
? NotUndefined<ParamList[RouteName]> extends NavigatorScreenParams<infer T>
? ParamListRoute<T>
: Route<Extract<RouteName, string>, ParamList[RouteName]>
: Route<Extract<RouteName, string>, ParamList[RouteName]>;
}[keyof ParamList];

type MaybeParamListRoute<ParamList extends {}> = ParamList extends ParamListBase
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Less than sure on the naming for this one. Need a second type to manage the case were ParamList does not extend ParamListBase, as the restriction on the ref is extends {}.

Would need the name to signify that it might be a "well typed" Route or it could be Route<string> depending on what you pass.

? ParamListRoute<ParamList>
: Route<string>;

export type NavigationContainerRef<ParamList extends {}> =
NavigationHelpers<ParamList> &
EventConsumer<NavigationContainerEventMap> & {
Expand All @@ -791,7 +805,7 @@ export type NavigationContainerRef<ParamList extends {}> =
/**
* Get the currently focused navigation route.
*/
getCurrentRoute(): Route<string> | undefined;
getCurrentRoute(): MaybeParamListRoute<ParamList> | undefined;
/**
* Get the currently focused route's options.
*/
Expand Down