Skip to content

Commit

Permalink
feat: automatically infer types for navigation in options, listeners …
Browse files Browse the repository at this point in the history
…etc. (#11883)

Co-authored-by: Mateusz Burzyński <mateuszburzynski@gmail.com>
  • Loading branch information
satya164 and Andarist committed Mar 12, 2024
1 parent 6e84eea commit c54baf1
Show file tree
Hide file tree
Showing 35 changed files with 832 additions and 230 deletions.
122 changes: 122 additions & 0 deletions example/__typechecks__/common.check.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import type {
import {
createStackNavigator,
type StackNavigationOptions,
type StackOptionsArgs,
type StackScreenProps,
} from '@react-navigation/stack';
import { expectTypeOf } from 'expect-type';
Expand Down Expand Up @@ -311,6 +312,79 @@ const SecondStack = createStackNavigator<SecondParamList>();
component={(_: { foo: number }) => <></>}
/>;

/**
* Check for options type in Screen config
*/
<SecondStack.Screen
name="HasParams1"
component={() => <></>}
options={{
headerShown: false,
}}
/>;

<SecondStack.Screen
name="HasParams1"
component={() => <></>}
options={{
// @ts-expect-error
headerShown: 13,
}}
/>;

<SecondStack.Screen
name="HasParams1"
component={() => <></>}
options={() => ({
headerShown: false,
})}
/>;

<SecondStack.Screen
name="HasParams1"
component={() => <></>}
// @ts-expect-error
options={() => ({
headerShown: 13,
})}
/>;

<SecondStack.Screen
name="HasParams1"
component={() => <></>}
options={({ route, navigation, theme }) => {
expectTypeOf(route.name).toEqualTypeOf<'HasParams1'>();
expectTypeOf(route.params).toEqualTypeOf<Readonly<{ id: string }>>();
expectTypeOf(navigation.getState().type).toMatchTypeOf<'stack'>();
expectTypeOf(navigation.push)
.parameter(0)
.toEqualTypeOf<keyof SecondParamList>();
expectTypeOf(theme).toMatchTypeOf<ReactNavigation.Theme>();

return {};
}}
/>;

<SecondStack.Screen
name="HasParams1"
component={() => <></>}
options={({
route,
navigation,
theme,
}: StackOptionsArgs<SecondParamList, 'HasParams1'>) => {
expectTypeOf(route.name).toEqualTypeOf<'HasParams1'>();
expectTypeOf(route.params).toEqualTypeOf<Readonly<{ id: string }>>();
expectTypeOf(navigation.getState().type).toMatchTypeOf<'stack'>();
expectTypeOf(navigation.push)
.parameter(0)
.toEqualTypeOf<keyof SecondParamList>();
expectTypeOf(theme).toMatchTypeOf<ReactNavigation.Theme>();

return {};
}}
/>;

/**
* Check for listeners type in Screen config
*/
Expand All @@ -334,6 +408,39 @@ const SecondStack = createStackNavigator<SecondParamList>();
}}
/>;

<SecondStack.Screen
name="HasParams1"
component={() => <></>}
listeners={({ route, navigation }) => {
expectTypeOf(route.name).toEqualTypeOf<'HasParams1'>();
expectTypeOf(route.params).toEqualTypeOf<Readonly<{ id: string }>>();
expectTypeOf(navigation.getState().type).toMatchTypeOf<'stack'>();
expectTypeOf(navigation.push)
.parameter(0)
.toEqualTypeOf<keyof SecondParamList>();

return {};
}}
/>;

<SecondStack.Screen
name="HasParams1"
component={() => <></>}
listeners={({
route,
navigation,
}: StackScreenProps<SecondParamList, 'HasParams1'>) => {
expectTypeOf(route.name).toEqualTypeOf<'HasParams1'>();
expectTypeOf(route.params).toEqualTypeOf<Readonly<{ id: string }>>();
expectTypeOf(navigation.getState().type).toMatchTypeOf<'stack'>();
expectTypeOf(navigation.push)
.parameter(0)
.toEqualTypeOf<keyof SecondParamList>();

return {};
}}
/>;

/**
* Check for errors with `navigation.navigate`
*/
Expand Down Expand Up @@ -376,3 +483,18 @@ export const ThirdScreen = ({
// @ts-expect-error
if (ScreenName === 'NoParams') navigation.navigate(ScreenName, { id: '123' });
};

/**
* Check for navigator ID
*/
type FourthParamList = {
HasParams1: { id: string };
HasParams2: { user: string };
NoParams: undefined;
};

const FourthStack = createStackNavigator<FourthParamList, 'MyID'>();

expectTypeOf(FourthStack.Navigator).parameter(0).toMatchTypeOf<{
id: 'MyID';
}>();
98 changes: 98 additions & 0 deletions example/__typechecks__/static.check.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,21 @@ createBottomTabNavigator({
screens: {},
});

createBottomTabNavigator({
screenOptions: () => ({
tabBarActiveTintColor: 'tomato',
}),
screens: {},
});

createBottomTabNavigator({
// @ts-expect-error
screenOptions: () => ({
tabBarActiveTintColor: 42,
}),
screens: {},
});

/**
* Infer screen options
*/
Expand All @@ -179,6 +194,29 @@ createBottomTabNavigator({
},
});

createBottomTabNavigator({
screens: {
Test: {
screen: () => null,
options: () => ({
tabBarActiveTintColor: 'tomato',
}),
},
},
});

createBottomTabNavigator({
screens: {
Test: {
screen: () => null,
// @ts-expect-error
options: () => ({
tabBarActiveTintColor: 42,
}),
},
},
});

createBottomTabNavigator({
screens: {
Test: {
Expand All @@ -190,6 +228,66 @@ createBottomTabNavigator({
},
});

/**
* Have correct type for screen options callback
*/
createBottomTabNavigator({
screenOptions: ({ route, navigation, theme }) => {
expectTypeOf(route.name).toMatchTypeOf<string>();
expectTypeOf(navigation.getState().type).toMatchTypeOf<'tab'>();
expectTypeOf(navigation.jumpTo).toMatchTypeOf<Function>();
expectTypeOf(theme).toMatchTypeOf<ReactNavigation.Theme>();

return {};
},
screens: {},
});

createBottomTabNavigator({
screens: {
Test: {
screen: () => null,
options: ({ route, navigation, theme }) => {
expectTypeOf(route.name).toMatchTypeOf<string>();
expectTypeOf(navigation.getState().type).toMatchTypeOf<'tab'>();
expectTypeOf(navigation.jumpTo).toMatchTypeOf<Function>();
expectTypeOf(theme).toMatchTypeOf<ReactNavigation.Theme>();

return {};
},
},
},
});

/**
* Have correct type for listeners callback
*/
createBottomTabNavigator({
screenListeners: ({ route, navigation }) => {
expectTypeOf(route.name).toMatchTypeOf<string>();
expectTypeOf(navigation.getState().type).toMatchTypeOf<'tab'>();
expectTypeOf(navigation.jumpTo).toMatchTypeOf<Function>();

return {};
},
screens: {},
});

createBottomTabNavigator({
screens: {
Test: {
screen: () => null,
listeners: ({ navigation, route }) => {
expectTypeOf(route.name).toMatchTypeOf<string>();
expectTypeOf(navigation.getState().type).toMatchTypeOf<'tab'>();
expectTypeOf(navigation.jumpTo).toMatchTypeOf<Function>();

return {};
},
},
},
});

/**
* Requires `screens` to be defined
*/
Expand Down
9 changes: 8 additions & 1 deletion example/src/Screens/NativeStackHeaderCustomization.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Button, HeaderButton } from '@react-navigation/elements';
import type { PathConfigMap } from '@react-navigation/native';
import {
createNativeStackNavigator,
type NativeStackOptionsArgs,
type NativeStackScreenProps,
} from '@react-navigation/native-stack';
import * as React from 'react';
Expand Down Expand Up @@ -120,7 +121,13 @@ export function NativeStackHeaderCustomization() {
<Stack.Screen
name="Article"
component={ArticleScreen}
options={({ route, navigation }) => ({
options={({
route,
navigation,
}: NativeStackOptionsArgs<
NativeHeaderCustomizationStackParams,
'Article'
>) => ({
title: `Article by ${route.params?.author ?? 'Unknown'}`,
headerTintColor: 'white',
headerTitle: ({ tintColor }) => (
Expand Down
18 changes: 10 additions & 8 deletions packages/bottom-tabs/src/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,36 @@
import {
createNavigationContainerRef,
NavigationContainer,
type ParamListBase,
} from '@react-navigation/native';
import { act, fireEvent, render } from '@testing-library/react-native';
import * as React from 'react';
import { Animated, Button, Text, View } from 'react-native';

import { type BottomTabScreenProps, createBottomTabNavigator } from '../index';

type BottomTabParamList = {
A: undefined;
B: undefined;
};

it('renders a bottom tab navigator with screens', async () => {
// @ts-expect-error: incomplete mock for testing
jest.spyOn(Animated, 'timing').mockImplementation(() => ({
start: (callback) => callback?.({ finished: true }),
}));

const Test = ({ route, navigation }: BottomTabScreenProps<ParamListBase>) => (
const Test = ({
route,
navigation,
}: BottomTabScreenProps<BottomTabParamList>) => (
<View>
<Text>Screen {route.name}</Text>
<Button onPress={() => navigation.navigate('A')} title="Go to A" />
<Button onPress={() => navigation.navigate('B')} title="Go to B" />
</View>
);

const Tab = createBottomTabNavigator();
const Tab = createBottomTabNavigator<BottomTabParamList>();

const { findByText, queryByText } = render(
<NavigationContainer>
Expand All @@ -42,11 +49,6 @@ it('renders a bottom tab navigator with screens', async () => {
expect(queryByText('Screen B')).not.toBeNull();
});

type BottomTabParamList = {
A: undefined;
B: undefined;
};

it('handles screens preloading', async () => {
const Tab = createBottomTabNavigator<BottomTabParamList>();

Expand Down
2 changes: 1 addition & 1 deletion packages/bottom-tabs/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,6 @@ export type {
BottomTabNavigationEventMap,
BottomTabNavigationOptions,
BottomTabNavigationProp,
BottomTabScreenOptions,
BottomTabOptionsArgs,
BottomTabScreenProps,
} from './types';
Loading

0 comments on commit c54baf1

Please sign in to comment.