Given the following object:
const ROUTES = {
PAGE_NO_PARAMS: '/hello/page/two',
PAGE_R: '/about/:id',
PAGE_Z: '/page/page/:param/:id',
PAGE_N: '/who/:x/:y/:z/page',
} as const
Is it possible a list of types / interfaces for each route
so that the developer is restricted to the valid params for the selected route.
Ie. generate a type from ROUTES
that has the same outcome as type RouteAndParams
below.
interface PageNoParams = {
route: '/hello/page/two' // no params
}
interface Param1 = {
route: '/about/:id',
params: { id: string } // required params
}
interface PAGE_Z = {
route: '/page/page/:param/:id',
params: { id: string; param: string } // required params
}
interface Param3 = {
route: '/who/:x/:y/:z/page',
params: { x: string; y: string; z: string } // required params
}
type RouteAndParams = PageNoParams | Param1 | PAGE_Z | Param3;
// Some examples of expected results / errors when using generated type
// should NOT error
const routeWithParams: RouteAndParams = {
route: '/page/page/:param/:id',
params: { 'param': 'blah', 'id': 'xxx' }
}
// should error as unexpected param 'x'
const routeWithParams: RouteAndParams = {
route: '/about/:id',
params: { 'id': 'xxx', 'x': 'xxx' }
}
// should error as param 'y' is missing
const routeWithParams: RouteAndParams = {
route: '/who/:x/:y/:z/page',
params: { 'x': 'blah', 'z': 'blah' }
}
I hope this all makes sense. I'm trying to replace runtime errors with build errors.
CodePudding user response:
You can use some recursive conditional types to process the path and extract the parameter from each path:
type Routes = MakeValidRoute<typeof ROUTES[keyof typeof ROUTES]>
// = "/hello/page/two" | `/about/${string}` | `/page/page/${string}/${string}` | `/who/${string}/${string}/${string}/page`
type GetInterfaceKeys<T extends string, R extends string = never> =
T extends `${string}/:${infer Name}/${infer Tail}`?
GetInterfaceKeys<`/${Tail}`, R | Name>:
T extends `${string}/:${infer Name}`?
R | Name:
R
type MakeRouteAndParams<T extends Record<string, string>> = {
[P in keyof T]: {
route: T[P],
params: Record<GetInterfaceKeys<T[P]>, string>
}
}[keyof T]
type RouteAndParams = MakeRouteAndParams<typeof ROUTES>
// type RouteAndParams = {
// route: "/hello/page/two";
// params: Record<never, string>;
// } | {
// route: "/about/:id";
// params: Record<"id", string>;
// } | {
// route: "/page/page/:param/:id";
// params: Record<"id" | "param", string>;
// } | {
// ...;
// }
We use tail recursive conditional types to improve performance of these types in the compiler
CodePudding user response:
Here's one approach:
type ParamNames<T extends string, A extends string = never> =
T extends `${infer F}:${infer E}/${infer R}` ?
ParamNames<R, A | E> : T extends `${infer F}:${infer E}` ?
A | E : A
ParamNames<T>
is a tail-recursive conditional type that turns a string including colon-delimited path parameters into a union of those parameter names. For example:
type Test = ParamNames<"/ay/:bee/cee/:dee">
// type Test = "bee" | "dee"
Then you can turn this into your route-and-params pairs with another conditional type:
type RouteAndParams<T extends string> = T extends unknown ?
ParamNames<T> extends infer S extends string ?
[S] extends [never] ?
{ route: T } :
{ route: T, params: { [K in S]: string } }
: never : never
It's a little fiddly because we suppress the params
property if ParamNames<T>
turns out to be never
for a particular member. Let's test it:
const ROUTES = {
PAGE_NO_PARAMS: '/hello/page/two',
PAGE_R: '/about/:id',
PAGE_Z: '/page/page/:param/:id',
PAGE_N: '/who/:x/:y/:z/page',
} as const
type RP = RouteAndParams<typeof ROUTES[keyof typeof ROUTES]>
/* type RP = {
route: "/hello/page/two";
} | {
route: "/about/:id";
params: {
id: string;
};
} | {
route: "/page/page/:param/:id";
params: {
id: string;
param: string;
};
} | {
route: "/who/:x/:y/:z/page";
params: {
x: string;
y: string;
z: string;
};
} */
Looks good.