I have an object:
config: {
someKey: someString
}
And I want to obtain a tuple type according to that config. So:
function createRouter<
T extends Record<string, string>
>(config: T) {
type Router = {
// How to get hold of T[K] below, such that, e.g.:
// ['someKey', typeof someString]
navigate: [keyof T, T[K]];
};
}
Expanding on this a little. I need this Router
type because I'm providing it to a function that returns an object of functions whose the parameters are based on the original config. In that function, the return object looks like this:
{
[P in keyof T]: (...params: Extract<T[P], readonly unknown[]>) => Promise<void>
}
Where T
in this case is the Router
type I'm passing in. An example usage is like this:
const router = createRouter({
user: 'user/:id'
});
router.navigate('user', { id: '123' });
In the example above, navigate
, and user
and id
are all inferred. The tuple [keyof T, T[K]]
is spread into the navigate
function, so keyof T
is the first param, and T[K}
is the second, (and I am also transforming T[K]
to extract the :id
parameter, but that doesn't matter for this question).
If config
has more than one key, then, the type should still be inferred correctly, so:
const router = createRouter({
user: 'user/:postId',
post: 'post/:postId'
});
router.navigate('user', { userId: '123' }); // all good
router.navigate('post', { postId: '123' }); // all good
router.navigate('post', { userId: '123' }); // error
Note that I'm not asking how to transform 'user/:userId'
, as I already have the type that converts that into an object type. I just need the config
in the form of a tuple
.
CodePudding user response:
So, you need to declare the config for the router as const, to let the generics trickle down. Then you need to split path by /
and only keep parts that include :${param}
. From here out, you construct an object with these keys.
type IsParameter<Part extends string> = Part extends `:${infer Anything}` ? Anything : never;
type FilteredParts<Path extends string> = Path extends `${infer PartA}/${infer PartB}`
? IsParameter<PartA> | FilteredParts<PartB>
: IsParameter<Path>;
interface MyRouter<T extends Record<string, string>> {
navigate<P extends keyof T>(
path: P,
options: Record<FilteredParts<T[P]>, string>,
): void;
};
declare function createRouter<
T extends Record<string, string>
>(config: T): MyRouter<T>;
const router = createRouter({
user: 'user/:userId',
post: 'post/:postId',
userPosts: 'user/:userId/post/:postId',
} as const);
router.navigate('user', { userId: '123' }); // all good
router.navigate('post', { postId: '123' }); // all good
router.navigate('userPosts', { postId: '123' }); // error userId missing
router.navigate('post', { userId: '123' }); // error
Playground: link
Source: https://lihautan.com/extract-parameters-type-from-string-literal-types-with-typescript/
CodePudding user response:
I'm not sure exactly what you want anymore, but I'll try help a bit.
The navigate
function needs to be generic to be able to infer the correct params for the route, so spreading a tuple type is not possible.
The following should at least be a good start:
type Router<Config extends Record<string, string>> = {
navigate<Route extends keyof Config>(
route: Route,
params: ExtractParams<Config[Route]>
): Promise<void>;
};
// The `Infer` for the parameter is to make TS infer the exact values of the properties, instead of just `string`.
// The builtin `Readonly` works, too. It just has to be a mapped type.
type Infer<T> = { [K in keyof T]: T[K] };
function createRouter<Config extends Record<string, string>>(
config: Infer<Config>
): Router<Config> {
return {} as Router<Config>;
}
const router = createRouter({
user: "user/:userId",
post: "post/:postId",
} as const);
router.navigate("user", { userId: "1" });
router.navigate("post", { postId: "2" });
As an aside, the following is my "parameter extractor" (without the optional params part):
/**
* Compute is a helper converting intersections of objects into
* flat, plain object types.
*
* @example
* Compute<{ a: string } & { b: string }> -> { a: string, b: string }
*/
export type Compute<obj> = { [k in keyof obj]: obj[k] } & unknown;
type CreateParamObject<keys extends string> = {
[k in keys]: string;
};
type ExtractParams<path extends string> = Compute<
path extends `${infer start}/:${infer param}/${infer rest}`
? CreateParamObject<param> &
ExtractParams<start> &
ExtractParams<`/${rest}`>
: path extends `${infer start}/:${infer param}`
? CreateParamObject<param> & ExtractParams<start>
: unknown
>;