Reputation: 1327
I have the following type:
type AllRoutes = {
Home: undefined
Page: { id: string }
}
type NavFunction<RouteName extends keyof AllRoutes> = (
...args: AllRoutes[RouteName] extends undefined
? [RouteName]
: [RouteName, AllRoutes[RouteName]]
) => void
If I put this type directly on a function like this is works as expected:
export function navigate<RouteName extends keyof AllRoutes>(
...args: AllRoutes[RouteName] extends undefined
? [RouteName]
: [RouteName, AllRoutes[RouteName]]
) {
...
}
navigate('Home') // works!
navigate('Page', {id: 1}) // works!
navigate('Page') // Expected 2 args
The issue I'm running into is that I would like to apply this type to multiple functions (each for a different platform). I can't seem to figure how how to apply the first type to a function and have it work like the "direct" example I just posted.
Upvotes: 3
Views: 182
Reputation: 3061
This is not a direct answer but a proposition to change the problem.
The expected usage of such functions is nice but requires a type not easy to maintain. What about using a whole object parameter with a discriminated union type?
// Routes discriminated by name
type HomeRoute = { name: 'Home' };
type PageRoute = { name: 'Page'; id: number };
type SearchRoute = { name: 'Search'; text: string; limit?: number };
type Route = HomeRoute | PageRoute | SearchRoute;
function navigateV1(route: Route): void { /*...*/ }
navigateV1({ name: 'Home' })
navigateV1({ name: 'Page', id: 1 })
navigateV1({ name: 'Page' }) // Got expected error "Property 'id' is missing..."
navigateV1({ name: 'Search', text: 'A*' })
The usage is just a bit more verbose but also gives more detailed error.
--
By the way, with the above discriminated union type, to stick to the required usage, we need also complex utility types:
// V2 (more complex to maintain) : arguments as deconstructed object
type RouteName = Route['name']; // "Home" | "Page" | "Search"
type RouteArgs<TRouteName extends RouteName,
TRoute = Extract<Route, { name: TRouteName }>,
TRest = Omit<TRoute, 'name'>> =
{} extends TRest
? [TRouteName]
: [TRouteName, TRest];
type TestHomeRouteArgs = RouteArgs<'Home'>; // ["Home"]
type TestPageRouteArgs = RouteArgs<'Page'>; // ["Page", Pick<PageRoute, "id">]
type TestSearchRouteArgs = RouteArgs<'Search'>; // ["Search", Pick<PageRoute, "text" | "limit">]
function navigateV2<TRouteName extends RouteName>(...args: RouteArgs<TRouteName>): void { /*...*/ }
navigateV2('Home')
navigateV2('Page', { id: 1 })
navigateV2('Page') // Got expected error but less precise: "Expected 2 arguments but got 1"
navigateV2('Search', { text: 'A*' })
// ----
// V3 : even more complex but output types more readable
type Prettify<T> = T extends infer Tbis ? { [K in keyof Tbis]: Tbis[K] } : never;
type PrettyRouteArgs<TRouteName extends RouteName,
TRoute = Extract<Route, { name: TRouteName }>,
TRest = Omit<TRoute, 'name'>> =
{} extends TRest
? [TRouteName]
: [TRouteName, Prettify<TRest>];
type TestPrettyHomeRouteArgs = PrettyRouteArgs<'Home'>; // ["Home"]
type TestPrettyPageRouteArgs = PrettyRouteArgs<'Page'>; // ["Page", { id: number; }]
type TestPrettySearchRouteArgs = PrettyRouteArgs<'Search'>; // ["Search", { text: string; limit?: number | undefined; }]
function navigateV3<TRouteName extends RouteName>(...args: PrettyRouteArgs<TRouteName>): void { /*...*/ }
navigateV3('Home')
navigateV3('Page', { id: 1 })
navigateV3('Page') // Got same expected error
navigateV3('Search', { text: 'A*' })
Upvotes: 2
Reputation: 32236
First, note that NavFunction
type, while similar to navigate
's function type, is in fact not the same. It should instead have the generic part attached to the function, not to the type name, like so:
// Note the generic part is now attached to the start of the function
// signature, rather than as a part of the NavFunction type name.
type NavFunction = <RouteName extends keyof AllRoutes>(
...args: AllRoutes[RouteName] extends undefined
? [RouteName]
: [RouteName, AllRoutes[RouteName]]
) => void
The difference being that in the original, NavFunction
requires you to specify which argument to expect up front, whereas in the revised version, it will be inferred when the function is used.
With that, you can use a function expression instead of a function declaration to attach the type:
const navigate: NavFunction = function(...args) {
// ...
}
navigate('Home') // works!
navigate('Page', {id: "1"}) // works!
navigate('Page') // Expected 2 args
But be aware that by doing it in this manner, the navigate
function will no longer be hoisted. :(
Upvotes: 2
Reputation: 15715
You can create a helper type for the args
portion. Maybe also create a type for keyof AllRoutes
?:
type RouteNames = keyof AllRoutes;
type GetArgs<RouteName extends RouteNames> = AllRoutes[RouteName] extends undefined
? [RouteName]
: [RouteName, AllRoutes[RouteName]];
export function navigate<RouteName extends RouteNames>(
...args: GetArgs<RouteName>
) {
...
}
Upvotes: 3