英文:
convert non-generic function to generic function with static types
问题
以下是您要翻译的部分:
To begin with, I have a tree of routes as follows:
interface RouteObject {
id: string;
path: string;
children?: RouteObject[];
}
const routeObjects: RouteObject[] = [
{
id: 'root',
path: '/',
children: [
{
id: 'auth',
path: 'auth',
children: [
{
id: 'login',
path: 'login',
children: [
{
id: 'vishal',
path: 'vishal',
},
],
},
{
id: 'register',
path: 'register',
},
{
id: 'resetPassword',
path: 'reset-password',
},
{
id: 'resendConfirmation',
path: 'resend-confirmation',
},
],
},
{
id: 'playground',
path: 'playground',
children: [
{
id: 'playgroundFormControls',
path: 'form-controls',
},
],
},
],
},
];
I am trying to achieve a flat object of id vs path as follows:
{
root: '/',
auth: '/auth',
login: '/auth/login',
vishal: '/auth/login/vishal',
register: '/auth/register',
resetPassword: '/auth/reset-password',
resendConfirmation: '/auth/resend-confirmation',
playground: '/playground',
playgroundFormControls: '/playground/form-controls'
}
Here is the Typescript code (without generics) that is capable of producing the output:
function createRoutes(routeObjects: RouteObject[], parentPath = '', routes: Record<string, string> = {}) {
for (let { id, path: relativePath, children } of routeObjects) {
let rootRelativePath =
relativePath === '/' || parentPath === '/' ? `${parentPath}${relativePath}` : `${parentPath}/${relativePath}`;
if (id) routes[id] = rootRelativePath;
if (children) createRoutes(children, rootRelativePath, routes);
}
return routes;
}
Here is the link of Playground: Working code without using Generics
The above code produces correctly, but cannot produce static types. By static types, I mean, the return type of createRoutes
should be an object with real ids and real paths, not just Record<string, string>
.
So, I tried to use generics, but cannot go farther than this: Generics non working playground
It will be helpful if someone can even point me in the right direction. Thanks.
英文:
To begin with, I have a tree of routes as follows:
interface RouteObject {
id: string;
path: string;
children?: RouteObject[];
}
const routeObjects: RouteObject[] = [
{
id: 'root',
path: '/',
children: [
{
id: 'auth',
path: 'auth',
children: [
{
id: 'login',
path: 'login',
children: [
{
id: 'vishal',
path: 'vishal',
},
],
},
{
id: 'register',
path: 'register',
},
{
id: 'resetPassword',
path: 'reset-password',
},
{
id: 'resendConfirmation',
path: 'resend-confirmation',
},
],
},
{
id: 'playground',
path: 'playground',
children: [
{
id: 'playgroundFormControls',
path: 'form-controls',
},
],
},
],
},
];
I am trying to achieve a flat object of id vs path as follows:
{
root: '/',
auth: '/auth',
login: '/auth/login',
vishal: '/auth/login/vishal',
register: '/auth/register',
resetPassword: '/auth/reset-password',
resendConfirmation: '/auth/resend-confirmation',
playground: '/playground',
playgroundFormControls: '/playground/form-controls'
}
Here is the Typescript code (without generics) that is capable of producing the output:
function createRoutes(routeObjects: RouteObject[], parentPath = '', routes: Record<string, string> = {}) {
for (let { id, path: relativePath, children } of routeObjects) {
let rootRelativePath =
relativePath === '/' || parentPath === '/' ? `${parentPath}${relativePath}` : `${parentPath}/${relativePath}`;
if (id) routes[id] = rootRelativePath;
if (children) createRoutes(children, rootRelativePath, routes);
}
return routes;
}
Here is the link of Playground: Working code without using Generics
The above code produces correctly, but cannot produce static types. By static types, I mean, the return type of createRoutes
should be an object with real ids and real paths, not just Record<string, string>
.
So, I tried to use generics, but cannot go farther than this: Generics non working playground
It will be helpful, if someone can even point me to the right direction. Thanks.
答案1
得分: 1
type RouteObject = {
id: string;
path: string;
children?: readonly RouteObject[];
};
declare function createRoutes<T extends readonly RouteObject[]>(
routeObjects: T
): Routes<T>;
type Routes<T extends readonly RouteObject[]> = {
[I in keyof T]: (x: Record<T[I]['id'], T[I]['path']> & (
T[I] extends {
path: infer P extends string;
children: infer R extends readonly RouteObject[];
} ? { [K in keyof Routes<R>]:
`${P extends `${infer PR}/` ? PR : P}/${Extract<Routes<R>[K], string>}`
} : {}
)) => void
}[number] extends (x: infer U) => void ? { [K in keyof U]: U[K] } : never;
const routeObjects = [
{
id: "root",
path: "/",
children: [
{
id: "auth",
path: "auth",
children: [
{
id: "login",
path: "login",
children: [
{
id: "vishal",
path: "vishal"
}
]
},
{
id: "register",
path: "register"
},
{
id: "resetPassword",
path: "reset-password"
},
{
id: "resendConfirmation",
path: "resend-confirmation"
}
]
}
]
},
{
id: "playground",
path: "playground",
children: [
{
id: "playgroundFormControls",
path: "form-controls"
}
]
}
];
const routes = createRoutes(routeObjects);
英文:
I'm going to operate under the assumption that id
and path
are required, and that you don't actually require children
to be a mutable array type, so my definition of RouteObject
is
type RouteObject = {
id: string,
path: string,
children?: readonly RouteObject[]
};
(Note that readonly
arrays are less restrictive than mutable ones, even though the name might imply otherwise.)
Also, in order for this to possibly work, you need the compiler to keep track of the string literal types of all the nested id
and path
properties of your route objects. If you annotate routeObjects
like this:
const routeObjects: RouteObject[] = [ ⋯ ];
then you are telling the compiler to throw away any more specific information from that initializing array literal. Even if you leave off the annotation like:
const routeObjects = [ ⋯ ];
the compiler will not infer anything more specific than string
for the nested id
and path
properties, since usually people don't want such narrow inference. Since we do want narrow inference, we should tell the compiler that via a const
assertion:
const routeObjects = [ ⋯ ] as const;
If you look at the type of routeObjects
now, you'll see that the compiler infers a quite specific readonly
tuple type:
/* const routeObjects: readonly [{
readonly id: "root";
readonly path: "/";
readonly children: readonly [{
readonly id: "auth";
readonly path: "auth";
readonly children: readonly [⋯] // omitted for brevity
}, {⋯}]; // omitted for brevity
}] */
and this is why I widened children
to allow readonly RouteObject[]
above; otherwise this would fail to be a RouteObject[]
and we'd have to play more games to get it to be accepted.
Anyway, now we're finally at a place where we can start to give strong typings.
I'm not going to touch the implementation of createRoutes()
much at all, since there's no way the compiler will ever be able to verify that it conforms to what will turn out to be quite a complex call signature. Instead I'll just hide the implementation behind a single-call-signature overload, or the equivalent. For the rest of the answer I'm not going to worry about implementation of the function and just look at the call signature:
declare function createRoutes<T extends readonly RouteObject[]>(
routeObjects: T
): Routes<T>;
So, createRoutes()
will be generic in the type T
of routeObjects
which has been constrained to readonly RouteObject[]
. And it returns an object of type Routes<T>
, which we will define as... oh boy here it comes:
type Routes<T extends readonly RouteObject[]> = {
[I in keyof T]: (x:
Record<T[I]['id'], T[I]['path']> & (
T[I] extends {
path: infer P extends string,
children: infer R extends readonly RouteObject[]
} ? { [K in keyof Routes<R>]:
`${P extends `${infer PR}/` ? PR : P}/${Extract<Routes<R>[K], string>}`
} : {}
)
) => void
}[number] extends (x: infer U) => void ?
{ [K in keyof U]: U[K] } : never;
That's quite complex, but possibly not any more complex than the type manipulation you want to see. It's a recursive type, where Routes<T>
is defined in terms of Routes<R>
for each R
corresponding to the children
property the elements of T
.
Let's break down what Routes<T>
does:
-
It walks through each element (at index
I
) of the tuple typeT
and makes an object type whose only key is theid
property and whose value at that key is thepath
property. That's whatRecord<T[I]['id'], T[I]['path']>
(using theRecord
utility type) means. So for the first (and only) element ofrouteObjects
it makes the equivalent of{root: "/"}
. -
If the element has a
children
propertyR
(which is checked via conditional type inference usingT[I] extends { ⋯ children: infer R ⋯ } ? ⋯
), it also recursively evaluates the object typeRoutes<R>
. So for that element it makes something like{ auth: "auth"; login: "auth/login"; vishal: "auth/login/vishal"; register: "auth/register"; ⋯ }
-
It takes
Routes<R>
and maps it to a version where we prepend the currentpath
property to it with a slash (unless it ends in a slash in which case we suppress the slash; this is a fiddly bit you might want to tweak depending on the implementation). That's what{ [K in keyof Routes<R>]: `${P extends `${infer PR}/` ? PR : P}/${Extract<Routes<R>[K], string>}`}
means. So for that element it makes something like{ auth: "/auth"; login: "/auth/login"; vishal: "/auth/login/vishal"; register: "/auth/register"; ⋯ }
-
It takes both of these object types and intersects them together. (Note that if the element has no
children
then we just intersect the empty object type{}
instead). -
Now what we do is collect all these individual pieces and intersect them all together to get one huge intersection
U
. This is done via the same technique asTupleToIntersection<T>
as described in the answer to "TypeScript merge generic array"; we put each piece in a contravariant type position, get a union of those, and then infer a single type from that position, which results in a huge intersection. That's what{ [I in keyof T]: (x: ⋯ ) => void}[number] extends (x: infer U) => void ? ⋯ : never
does. So if walking through the array gives us something like[{a: "b"} & {c: "b/d"}, {e: "f"} & {g: "f/h"}]
, then it is converted into the single intersection{a: "b"} & {c: "b/d"} & {e: "f"} & {g: "f/h"}
-
Finally because a huge intersection is ugly, we collect them into a single object type by doing a simple identity mapped type over
U
; that's what{ [K in keyof U]: U[K] }
does. So ifU
is{a: "b"} & {c: "b/d"} & {e: "f"} & {g: "f/h"}
, then the output is{a: "b"; c: "b/d"; e: "f"; g: "f/h"}
.
Whew, that was rough. Okay, let's test it:
const routes = createRoutes(routeObjects);
/* const routes: {
root: "/";
auth: "/auth";
login: "/auth/login";
vishal: "/auth/login/vishal";
register: "/auth/register";
resetPassword: "/auth/reset-password";
resendConfirmation: "/auth/resend-confirmation";
playground: "/playground";
playgroundFormControls: "/playground/form-controls";
} */
Hooray, that's exactly what you wanted to see.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论