将非泛型函数转换为具有静态类型的泛型函数

huangapple go评论63阅读模式
英文:

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: &#39;root&#39;,
    path: &#39;/&#39;,
    children: [
      {
        id: &#39;auth&#39;,
        path: &#39;auth&#39;,
        children: [
          {
            id: &#39;login&#39;,
            path: &#39;login&#39;,
            children: [
              {
                id: &#39;vishal&#39;,
                path: &#39;vishal&#39;,
              },
            ],
          },
          {
            id: &#39;register&#39;,
            path: &#39;register&#39;,
          },
          {
            id: &#39;resetPassword&#39;,
            path: &#39;reset-password&#39;,
          },
          {
            id: &#39;resendConfirmation&#39;,
            path: &#39;resend-confirmation&#39;,
          },
        ],
      },
      {
        id: &#39;playground&#39;,
        path: &#39;playground&#39;,
        children: [
          {
            id: &#39;playgroundFormControls&#39;,
            path: &#39;form-controls&#39;,
          },
        ],
      },
    ],
  },
];

I am trying to achieve a flat object of id vs path as follows:

{
root: &#39;/&#39;,
auth: &#39;/auth&#39;,
login: &#39;/auth/login&#39;,
vishal: &#39;/auth/login/vishal&#39;,
register: &#39;/auth/register&#39;,
resetPassword: &#39;/auth/reset-password&#39;,
resendConfirmation: &#39;/auth/resend-confirmation&#39;,
playground: &#39;/playground&#39;,
playgroundFormControls: &#39;/playground/form-controls&#39;
} 

Here is the Typescript code (without generics) that is capable of producing the output:

function createRoutes(routeObjects: RouteObject[], parentPath = &#39;&#39;, routes: Record&lt;string, string&gt; = {}) {
  for (let { id, path: relativePath, children } of routeObjects) {
    let rootRelativePath =
      relativePath === &#39;/&#39; || parentPath === &#39;/&#39; ? `${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&lt;string, string&gt;.

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: &quot;root&quot;;
readonly path: &quot;/&quot;;
readonly children: readonly [{
readonly id: &quot;auth&quot;;
readonly path: &quot;auth&quot;;
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&lt;T extends readonly RouteObject[]&gt;(
routeObjects: T
): Routes&lt;T&gt;;

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&lt;T&gt;, which we will define as... oh boy here it comes:

type Routes&lt;T extends readonly RouteObject[]&gt; = {
[I in keyof T]: (x:
Record&lt;T[I][&#39;id&#39;], T[I][&#39;path&#39;]&gt; &amp; (
T[I] extends {
path: infer P extends string,
children: infer R extends readonly RouteObject[]
} ? { [K in keyof Routes&lt;R&gt;]:
`${P extends `${infer PR}/` ? PR : P}/${Extract&lt;Routes&lt;R&gt;[K], string&gt;}`
} : {}
)
) =&gt; void
}[number] extends (x: infer U) =&gt; 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&lt;T&gt; is defined in terms of Routes&lt;R&gt; for each R corresponding to the children property the elements of T.

Let's break down what Routes&lt;T&gt; does:

  • It walks through each element (at index I) of the tuple type T and makes an object type whose only key is the id property and whose value at that key is the path property. That's what Record&lt;T[I][&#39;id&#39;], T[I][&#39;path&#39;]&gt; (using the Record utility type) means. So for the first (and only) element of routeObjects it makes the equivalent of {root: &quot;/&quot;}.

  • If the element has a children property R (which is checked via conditional type inference using T[I] extends { ⋯ children: infer R ⋯ } ? ⋯ ), it also recursively evaluates the object type Routes&lt;R&gt;. So for that element it makes something like { auth: &quot;auth&quot;; login: &quot;auth/login&quot;; vishal: &quot;auth/login/vishal&quot;; register: &quot;auth/register&quot;; ⋯ }

  • It takes Routes&lt;R&gt; and maps it to a version where we prepend the current path 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&lt;R&gt;]: `${P extends `${infer PR}/` ? PR : P}/${Extract&lt;Routes&lt;R&gt;[K], string&gt;}`} means. So for that element it makes something like { auth: &quot;/auth&quot;; login: &quot;/auth/login&quot;; vishal: &quot;/auth/login/vishal&quot;; register: &quot;/auth/register&quot;; ⋯ }

  • 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 as TupleToIntersection&lt;T&gt; 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: ⋯ ) =&gt; void}[number] extends (x: infer U) =&gt; void ? ⋯ : never does. So if walking through the array gives us something like [{a: &quot;b&quot;} &amp; {c: &quot;b/d&quot;}, {e: &quot;f&quot;} &amp; {g: &quot;f/h&quot;}], then it is converted into the single intersection {a: &quot;b&quot;} &amp; {c: &quot;b/d&quot;} &amp; {e: &quot;f&quot;} &amp; {g: &quot;f/h&quot;}

  • 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 if U is {a: &quot;b&quot;} &amp; {c: &quot;b/d&quot;} &amp; {e: &quot;f&quot;} &amp; {g: &quot;f/h&quot;}, then the output is {a: &quot;b&quot;; c: &quot;b/d&quot;; e: &quot;f&quot;; g: &quot;f/h&quot;}.

Whew, that was rough. Okay, let's test it:

const routes = createRoutes(routeObjects);
/* const routes: {
root: &quot;/&quot;;
auth: &quot;/auth&quot;;
login: &quot;/auth/login&quot;;
vishal: &quot;/auth/login/vishal&quot;;
register: &quot;/auth/register&quot;;
resetPassword: &quot;/auth/reset-password&quot;;
resendConfirmation: &quot;/auth/resend-confirmation&quot;;
playground: &quot;/playground&quot;;
playgroundFormControls: &quot;/playground/form-controls&quot;;
} */

Hooray, that's exactly what you wanted to see.

Playground link to code

huangapple
  • 本文由 发表于 2023年4月7日 01:45:04
  • 转载请务必保留本文链接:https://go.coder-hub.com/75952361.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定