Generics, Typescript infer params type from prop value

1 week ago 17
ARTICLE AD BOX

I have a message component that looks like this:

type MessageProps< ParamList extends Record<string, any>, K extends keyof ParamList > = { navTo: K; title: string; desc: string; cta: string; } & (undefined extends ParamList[K] ? { params?: ParamList[K] } : { params: ParamList[K] }); export function Message<ParamList extends Record<string, any>, K extends keyof ParamList = keyof ParamList>( props: MessageProps<ParamList, K> ) { const { navTo, params, title, desc, cta } = props; const navigation = useNavigation<NativeStackNavigationProp<ParamList, K>>(); const handlePress = () => { if (params === undefined) { // @ts-ignore navigation.replace(navTo); } else { // @ts-ignore navigation.replace(navTo, params); } }; return ( <OnboardingStep title={title} desc={desc} nextBtn={ <Button variant="primary" size="large" theme="light" title={cta} onPress={handlePress} /> } /> ); }

and is will be used like so:

<Message<AppOnboardingNavigationParamList> navTo="MessageCustomer2" title="No more loyalty card mess for you!" desc="All your cards will be stored in the same place" cta="Next" /> <Message<AppOnboardingNavigationParamList, "Email"> navTo="Email" params={{ title: "", desc: "" }} title="Fast stamping" desc="Stamps are quickly added when you show your unique code" cta="Next" />

Ideally i would like ts to infer the type of the params prop based on the navTo prop value. instead of having to supply the key in the generic type. So instead of this: <Message<AppOnboardingNavigationParamList, "Email"> i would like just this: <Message<AppOnboardingNavigationParamList>

But i cant get it to work with ts, the params type is a union of all possible ParamList[K] or it doesnt provide any type information at all. I am out of my league here and would love some help or even to know if it is possible at all.

currenly if i just do: <Message<AppOnboardingNavigationParamList> I get this as an auto complete suggestion from ts:

(property) params?: { name: string; } | { title: string; desc: string; } | { email: string; } | undefined

where it should ideally be (which i do get if i use <Message<AppOnboardingNavigationParamList, "email">)

{ title: string; desc: string; }

Next to this i keep getting errors on the navigation.replace(navTo); or navigation.replace(navTo, params);

The errors on the navigation.replace look like:

Argument of type '[K]' is not assignable to parameter of type 'K extends unknown ? undefined extends ParamList[K] ? [screen: K, params?: ParamList[K] | undefined] : [screen: K, params: ParamList[K]] : never'.

See below the minimal code for this to work, you'd need to add this to a react native project thought with react navigation.

export type AppOnboardingNavigationParamList = { Hello: undefined; Name: undefined; CustomerType: { name: string }; MessageCustomer1: undefined; MessageCustomer2: undefined; MessageBusiness1: undefined; MessageBusiness2: undefined; Email: { title: string, desc: string }; } function Hello() { return null; } function Name() { return null; } function CustomerType(props: NativeStackScreenProps<AppOnboardingNavigationParamList, 'CustomerType'>) { return null; } function Email(props: NativeStackScreenProps<AppOnboardingNavigationParamList, 'Email'>) { return null; } export function Message<ParamList extends Record<string, any>, K extends keyof ParamList = keyof ParamList>( props: MessageProps<ParamList, K> ) { const { navTo, params, title, desc, cta } = props; const navigation = useNavigation<NativeStackNavigationProp<ParamList, K>>(); const handlePress = () => { if (params === undefined) { // @ts-ignore navigation.replace(navTo); } else { // @ts-ignore navigation.replace(navTo, params); } }; return ( <Button title = { cta } onPress = { handlePress } /> ); } const OnboardingStack = createNativeStackNavigator<AppOnboardingNavigationParamList>(); export default function UserOnboarding() { return ( <OnboardingStack.Navigator screenOptions= {{ headerShown: false } } initialRouteName = "Hello" > <OnboardingStack.Screen name='Hello' component = { Hello } /> <OnboardingStack.Screen name='Name' component = { Name } /> <OnboardingStack.Screen name='CustomerType' component = { CustomerType } /> <OnboardingStack.Screen name='MessageCustomer1' children = {() => ( <Message<AppOnboardingNavigationParamList> navTo= "MessageCustomer2" title = "No more loyalty card mess for you!" desc = "All your cards will be stored in the same place" cta = "Next" /> )} /> < OnboardingStack.Screen name = 'MessageCustomer2' children = {() => ( <Message<AppOnboardingNavigationParamList> navTo= "Email" params = {{ title: "", desc: "" }} title = "Fast stamping" desc = "Stamps are quickly added when you show your unique code" cta = "Next" /> )} /> < OnboardingStack.Screen name = 'MessageBusiness1' children = {() => ( <Message<AppOnboardingNavigationParamList> navTo= "MessageBusiness2" title = "No more loyalty card mess for your clients" desc = "All your cards will be stored in the same place" cta = "Next" /> )} /> < OnboardingStack.Screen name = 'MessageBusiness2' children = {() => ( <Message<AppOnboardingNavigationParamList, "Email"> navTo= "Email" title = "Make your customers come " desc = "Create fidelity programs to generate more engagement and retention" cta = "Next" params = {{ title: "", desc: "" }} /> )} /> < OnboardingStack.Screen name = 'Email' component = { Email } /> </OnboardingStack.Navigator> ) }
Read Entire Article