Skip to content

Commit

Permalink
chore: tweak checks for correct types
Browse files Browse the repository at this point in the history
  • Loading branch information
satya164 committed Nov 27, 2022
1 parent 8850c51 commit 05668d2
Show file tree
Hide file tree
Showing 3 changed files with 144 additions and 122 deletions.
9 changes: 3 additions & 6 deletions example/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,7 @@ import { SafeAreaView } from 'react-native-safe-area-context';
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';

import { restartApp } from './Restart';
import {
RootDrawerParamList,
RootStackParamList,
SCREEN_NAMES,
SCREENS,
} from './screens';
import { RootDrawerParamList, RootStackParamList, SCREENS } from './screens';
import NotFound from './Screens/NotFound';
import SettingsItem from './Shared/SettingsItem';

Expand All @@ -64,6 +59,8 @@ const Stack = createStackNavigator<RootStackParamList>();
const NAVIGATION_PERSISTENCE_KEY = 'NAVIGATION_STATE';
const THEME_PERSISTENCE_KEY = 'THEME_TYPE';

const SCREEN_NAMES = Object.keys(SCREENS) as (keyof typeof SCREENS)[];

export default function App() {
const [theme, setTheme] = React.useState(DefaultTheme);

Expand Down
32 changes: 6 additions & 26 deletions example/src/screens.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,107 +27,87 @@ export type LinkComponentDemoParamList = {
Albums: undefined;
};

const paramsType = <params,>() => undefined as unknown as params;

export const SCREENS = {
NativeStack: {
title: 'Native Stack',
component: NativeStack,
params: undefined,
},
SimpleStack: {
title: 'Simple Stack',
component: SimpleStack,
params: undefined,
},
ModalStack: {
title: 'Modal Stack',
component: ModalStack,
params: undefined,
},
MixedStack: {
title: 'Regular + Modal Stack',
component: MixedStack,
params: undefined,
},
MixedHeaderMode: {
title: 'Float + Screen Header Stack',
component: MixedHeaderMode,
params: undefined,
},
StackTransparent: {
title: 'Transparent Stack',
component: StackTransparent,
params: undefined,
},
StackHeaderCustomization: {
title: 'Header Customization in Stack',
component: StackHeaderCustomization,
params: undefined,
},
NativeStackHeaderCustomization: {
title: 'Header Customization in Native Stack',
component: NativeStackHeaderCustomization,
params: undefined,
},
BottomTabs: {
title: 'Bottom Tabs',
component: BottomTabs,
params: undefined,
},
MaterialTopTabs: {
title: 'Material Top Tabs',
component: MaterialTopTabsScreen,
params: undefined,
},
MaterialBottomTabs: {
title: 'Material Bottom Tabs',
component: MaterialBottomTabs,
params: undefined,
},
DynamicTabs: {
title: 'Dynamic Tabs',
component: DynamicTabs,
params: undefined,
},
MasterDetail: {
title: 'Master Detail',
component: MasterDetail,
params: undefined,
},
AuthFlow: {
title: 'Auth Flow',
component: AuthFlow,
params: undefined,
},
StackPreventRemove: {
title: 'Prevent removing screen in Stack',
component: StackPreventRemove,
params: undefined,
},
NativeStackPreventRemove: {
title: 'Prevent removing screen in Native Stack',
component: NativeStackPreventRemove,
params: undefined,
},
LinkComponent: {
title: '<Link />',
component: LinkComponent,
params: paramsType<
NavigatorScreenParams<LinkComponentDemoParamList> | undefined
>(),
},
};

export const SCREEN_NAMES = Object.keys(SCREENS) as (keyof typeof SCREENS)[];

export type RootStackParamList = {
type ParamListTypes = {
Home: undefined;
NotFound: undefined;
} & {
[P in keyof typeof SCREENS]: typeof SCREENS[P]['params'];
LinkComponent: NavigatorScreenParams<LinkComponentDemoParamList> | undefined;
};

export type RootStackParamList = {
[P in Exclude<keyof typeof SCREENS, keyof ParamListTypes>]: undefined;
} & ParamListTypes;

// Make the default RootParamList the same as the RootStackParamList
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
Expand Down
225 changes: 135 additions & 90 deletions example/types.check.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,18 @@ import type {
CompositeScreenProps,
NavigationHelpers,
NavigatorScreenParams,
RouteConfigComponent,
RouteProp,
} from '@react-navigation/native';
import type { StackNavigationOptions } from '@react-navigation/stack';
import {
createStackNavigator,
StackScreenProps,
} from '@react-navigation/stack';
import { expectTypeOf } from 'expect-type';
import React, { FC } from 'react';
import * as React from 'react';

/**
* Check for the type of the `navigation` and `route` objects with regular usage
*/
type RootStackParamList = {
Home: NavigatorScreenParams<HomeDrawerParamList>;
PostDetails: { id: string; section?: string };
Expand Down Expand Up @@ -216,98 +217,142 @@ export const LatestScreen = ({
.toEqualTypeOf<'LeftDrawer' | 'BottomTabs' | undefined>();
};

// ================================================
// The checks below uses jest syntax for semantics only.
// These tests will never actually run.
// Notice the use of // @ts-expect-error for assertions
// instead of `expectTypeOf`. This is because `expect-type` fails
// to throw errors in various cases involving parameters or union types.
// ================================================

describe('RouteConfigComponent', () => {
type ParamList = {
hasParam: { param: string };
hasParam2: { param2: string };
noParam: undefined;
};

const Screen = <Name extends keyof ParamList>(
_: { name: Name } & RouteConfigComponent<ParamList, Name>
) => null;

it("doesn't accept incorrect route params", () => {
const Component: FC<{ route: RouteProp<ParamList, 'hasParam'> }> = () =>
null;
// @ts-expect-error
<Screen name="hasParam2" component={Component} />;
// @ts-expect-error
<Screen name="noParam" component={Component} />;
// ok
<Screen name="hasParam" component={Component} />;
});
/**
* Check for errors when the screen component isn't typed correctly
*/
type SecondParamList = {
HasParams1: { id: string };
HasParams2: { user: string };
NoParams: undefined;
};

it("doesn't require the component to accept the `route` or `navigation` prop", () => {
const Component: FC<{}> = () => null;
// ok
<Screen name="hasParam" component={Component} />;
// ok
<Screen name="noParam" component={Component} />;
});
const SecondStack = createStackNavigator<SecondParamList>();

// No error when type for props is correct
<SecondStack.Screen
name="HasParams1"
component={(_: StackScreenProps<SecondParamList, 'HasParams1'>) => <></>}
/>;

<SecondStack.Screen
name="HasParams1"
component={(_: { route: { params: { id: string } } }) => <></>}
/>;

<SecondStack.Screen
name="NoParams"
component={(_: StackScreenProps<SecondParamList, 'NoParams'>) => <></>}
/>;

<SecondStack.Screen
name="NoParams"
component={(_: { route: { params?: undefined } }) => <></>}
/>;

// No error when the component hasn't specified params
<SecondStack.Screen
name="HasParams1"
component={(_: { route: {} }) => <></>}
/>;

// No error when the component has specified params as optional
<SecondStack.Screen
name="HasParams1"
component={(_: { route: { params: { id?: string } } }) => <></>}
/>;

// No error when the component doesn't take route prop
<SecondStack.Screen name="HasParams1" component={() => <></>} />;

<SecondStack.Screen
name="HasParams1"
component={(_: { navigation: unknown }) => <></>}
/>;

<SecondStack.Screen name="NoParams" component={() => <></>} />;

<SecondStack.Screen
name="NoParams"
component={(_: { navigation: unknown }) => <></>}
/>;

// Error if props don't match type
<SecondStack.Screen
// @ts-expect-error
name="HasParams1"
component={(_: StackScreenProps<SecondParamList, 'HasParams2'>) => <></>}
/>;

<SecondStack.Screen
name="HasParams1"
// @ts-expect-error
component={(_: { route: { params: { ids: string } } }) => <></>}
/>;

<SecondStack.Screen
// @ts-expect-error
name="NoParams"
component={(_: StackScreenProps<SecondParamList, 'HasParams1'>) => <></>}
/>;

<SecondStack.Screen
name="NoParams"
// @ts-expect-error
component={(_: { route: { params: { ids: string } } }) => <></>}
/>;

// Error if component specifies a prop other than route or navigation
<SecondStack.Screen
name="HasParams1"
// @ts-expect-error
component={(_: { foo: number }) => <></>}
/>;

<SecondStack.Screen
name="NoParams"
// @ts-expect-error
component={(_: { foo: number }) => <></>}
/>;

/**
* Check for errors with `navigation.navigate`
*/
type ThirdParamList = {
HasParams1: { id: string };
HasParams2: { user: string };
NoParams: undefined;
};

it('allows the component to declare any optional props', () => {
const Component: FC<{ someProp?: string }> = () => null;
<Screen name="hasParam" component={Component} />;
<Screen name="noParam" component={Component} />;
});
export const ThirdScreen = ({
navigation,
}: {
navigation: NavigationHelpers<ThirdParamList>;
}) => {
// No error when correct params are passed
navigation.navigate('NoParams');
navigation.navigate('HasParams1', { id: '123' });
navigation.navigate('HasParams2', { user: '123' });

it("doesn't allow a required prop that's neither `route` nor `navigation`", () => {
const Component: FC<{ someProp: string }> = () => null;
// @ts-expect-error
<Screen name="hasParam" component={Component} />;
// @ts-expect-error
<Screen name="noParam" component={Component} />;
});
// Error when incorrect params are passed
// @ts-expect-error
navigation.navigate('HasParams1', { user: '123' });
// @ts-expect-error
navigation.navigate('HasParams2', { id: '123' });

it('allows the component to accept just the `navigation` prop', () => {
const Component: FC<{ navigation: object }> = () => null;
// ok
<Screen name="hasParam" component={Component} />;
// ok
<Screen name="noParam" component={Component} />;
});
});

describe('NavigationHelpers.navigate', () => {
type ParamList = {
hasParam: { param: string };
hasParam2: { param2: string };
noParam: undefined;
};
const navigate: NavigationHelpers<ParamList>['navigate'] = () => {};

it('strictly checks type of route params', () => {
// ok
navigate('noParam');
// ok
navigate('hasParam', { param: '123' });
// @ts-expect-error
navigate('hasParam2', { param: '123' });
});
// Union type and type narrowing
const ScreenName: keyof ThirdParamList = null as any;

it('strictly checks type of route params when a union RouteName is passed', () => {
let routeName = undefined as unknown as keyof ParamList;
// @ts-expect-error
navigation.navigate(ScreenName);

// @ts-expect-error
navigate(routeName);
// @ts-expect-error
if (ScreenName === 'HasParams1') navigation.navigate(ScreenName);

// ok
if (routeName === 'noParam') navigate(routeName);
// ok
if (routeName === 'hasParam') navigate(routeName, { param: '123' });
if (ScreenName === 'HasParams1')
navigation.navigate(ScreenName, { id: '123' });

// @ts-expect-error
if (routeName === 'hasParam') navigate(routeName);
// @ts-expect-error
if (routeName === 'hasParam2') navigate(routeName, { param: '123' });
});
});
if (ScreenName === 'NoParams') navigation.navigate(ScreenName);

// @ts-expect-error
if (ScreenName === 'NoParams') navigation.navigate(ScreenName, { id: '123' });
};

0 comments on commit 05668d2

Please sign in to comment.