Transform a specialized function into a generic function with static typing

First off, I have a network of routes structured like this:

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 working on converting this structure into a flat object containing id vs path pairs:

{
  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'
} 

Below is the Typescript function (excluding generics) that achieves the desired 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;
}

You can test the above code using this link to Playground.

The current implementation functions correctly but lacks static types. By static types, I mean that the return type of createRoutes should consist of real ids and paths instead of just Record<string, string>.

I attempted to incorporate generics, yet my progress hit a roadblock here: Generics non working playground

If anyone could guide me in the right direction, it would be greatly appreciated. Thank you.

Answer №1

Assuming that the id and path are necessary, and that children do not have to be a mutable array type, my definition of RouteObject is as follows:

type RouteObject = {
  id: string,
  path: string,
  children?: readonly RouteObject[]
};

(It's worth noting that readonly arrays offer less restriction than mutable ones despite the name.)


In order for this setup to function properly, it is essential for the compiler to keep track of the string literal types associated with all nested id and path properties in your route objects. By annotating routeObjects like this:

const routeObjects: RouteObject[] = [ ⋯ ];

You are instructing the compiler to discard any specific information from that initializing array literal. Even without the annotation, if you define it as:

const routeObjects = [ ⋯ ];

The compiler will default to inferring just string for the nested id and path properties since narrow inference is usually preferred. To achieve precise inference, a const assertion can be used:

const routeObjects = [ ⋯ ] as const;

With this approach, the type of routeObjects reflects a highly 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
}] */

This is why I extended children to allow readonly RouteObject[]; otherwise, it would fail to qualify as a RouteObject[].

Now, we are equipped with a strong typing foundation.


The implementation specifics of createRoutes() remain largely untouched as verifying its conformance to what could potentially be a complex call signature is unfeasible by the compiler. Instead, the focus shifts towards encapsulating the implementation within a single-call-signature overload, or an equivalent construct. Moving forward, attention is diverted toward the call signature rather than the actual function implementation:

declare function createRoutes<T extends readonly RouteObject[]>(
  routeObjects: T
): Routes<T>;

Hence, createRoutes() evolves into a generic function contingent upon the type T pertaining to routeObjects constrained within readonly RouteObject[]. It yields an object of type Routes<T>, characterized by intricate structure manipulation:

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;

Despite its complexity, this recursive type defines Routes<T> through iterations involving Routes<R> for each R corresponding to the children property of elements in T.

Let us dissect the functionality of Routes<T>:

  • An object type is constructed for each element (indexed by I) within the tuple type

    T</code, wherein the sole key represents the <code>id
    property mapping to the respective path property - articulated through
    Record<<T[I]['id'], T[I]['path']>
    using the Record utility type. For instance, the initial element in routeObjects translates to {root: "/"}.

  • If the element encompasses a children property R (verified via conditional type inference leveraging

    T[I] extends { ⋯ children: infer R ⋯ } ? ⋯ 
    ), a recursive evaluation of the object type Routes<R> is also carried out. Consequently, an output akin to
    { auth: "auth"; login: "auth/login"; vishal: "auth/login/vishal"; register: "auth/register"; ⋯ }
    materializes for such elements.

  • Subsequently, Routes<R> undergos mapping where the current path property is prepended with a slash (but avoids adding a redundant slash if already present at the end; tweaking may be required depending on the implementation). This metamorphosis is depicted through

    { [K in keyof Routes<R>]: `${P extends `${infer PR}/` ? PR : P}/${Extract<Routes<R>[K], string>}`}
    . Thus, elements transform along lines of
    { auth: "/auth"; login: "/auth/login"; vishal: "/auth/login/vishal"; register: "/auth/register"; ⋯ }
    .

  • Both these object types converge through an intersection. In cases where no children exist, intersection involves solely an empty object type {}.

  • All constituent pieces amalgamate resulting in a substantial union U. Employing techniques akin to

    TupleToIntersection<T></code elaborated in response to <a href="https://stackoverflow.com/a/74202280/2887218">TypeScript merge generic array</a>, individual segments are placed in a <a href="https://en.wikipedia.org/wiki/Covariance_and_contravariance_(computer_science)" rel="nofollow noreferrer">contravariant</a> type stance, consolidated into a union, and eventually distilled into one encompassing intersection. The process is aptly captured within <code>{ [I in keyof T]: (x: ⋯ ) => void}[number] extends (x: infer U) ⇒ void ? ⋯ : never
    . Hence, if navigating through the array yields
    [{a: "b"} & {c: "b/d"}, {e: "f"} & {g: "f/h"}]
    , culmination occurs in the unified interaction of
    {a: "b"} & {c: "b/d"} & { e: "f"} & {g: "f/h"}
    .

  • To enhance readability amidst a large intersection type, a direct identity-mapping lineage over U leads to consolidation into a singular object type - evidently portrayed progress through { [K in keyof U]: U[K] }. A scenario representing

    {a: "b"} & {c: "b/d"} & {e: "f"} & {g: "f/h"}
    would culminate into
    {a: "b"; c: "b/d"; e: "f"; g: "f/h"}
    .

A comprehensive breakdown indeed! Time for validation:

const routes = createRoutes(routeObjects);
/* Outputting:
{
    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";
}
*/

Mission accomplished - delivering exactly the insight anticipated.

Playground link to code

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Leveraging Angular2's observable stream in combination with *ngFor

Below is the code snippet I am working with: objs = [] getObjs() { let counter = 0 this.myService.getObjs() .map((obj) => { counter = counter > 5 ? 0 : counter; obj.col = counter; counter++; return view ...

I had high hopes that TypeScript's automatic type inference for constructor parameters would do the trick, but it seems to have let

You can experiment with the given code snippet at the online playground to confirm it. Consider this code: class Alpha { private beta; constructor(b: Beta) { this.beta = b; } doSomething() { ...

Spotlight a newly generated element produced by the*ngFor directive within Angular 2

In my application, I have a collection of words that are displayed or hidden using *ngFor based on their 'hidden' property. You can view the example on Plunker. The issue arises when the word list becomes extensive, making it challenging to ide ...

Resolving issues with Typescript declarations for React Component

Currently utilizing React 16.4.1 and Typescript 2.9.2, I am attempting to use the reaptcha library from here. The library is imported like so: import * as Reaptcha from 'reaptcha'; Since there are no type definitions provided, building results ...

Find the length of time in Typescript (measured in hours, minutes, and seconds)

Trying to calculate the duration between two dates in TypeScript (Angular): 2021-11-19 21:59:59 and 2021-11-19 22:00:18 let startDate: Date = new Date(start); let endDate: Date = new Date(end); if(end != null) { let duration = new Date(endDate.getT ...

What is the best method for eliminating the .vue extension in TypeScript imports within Vue.JS?

I recently created a project using vue-cli3 and decided to incorporate TypeScript for added type safety. Here is a snippet from my src/app.vue file: <template> <div id="app"> <hello-world msg="test"/> </div> </template& ...

Using TypeORM with a timestamp type column set to default null can lead to an endless loop of migrations being

In my NestJs project using TypeORM, I have the following column definition in an entity: @CreateDateColumn({ nullable: true, type: 'timestamp', default: () => 'NULL', }) public succeededAt?: Date; A migration is gene ...

I am currently unable to retrieve any results for my fullcalendar tooltip

I am currently working on setting tooltips for events using Primeng's fullcalendar. Despite initializing the tooltip in the web console, I am unable to see it when hovering over an event. My development environment includes Typescript, Primeng 7.0.5, ...

Challenge encountered with TypeScript integration in the controller

I am currently in the process of converting a website from VB to C# and incorporating TypeScript. I have successfully managed to send the data to the controller. However, instead of redirecting to the next page, the controller redirects back to the same pa ...

I'm puzzled by the error message stating that '<MODULE>' is declared locally but not exported

I am currently working with a TypeScript file that exports a function for sending emails using AWS SES. //ses.tsx let sendEmail = (args: sendmailParamsType) => { let params = { //here I retrieve the parameters from args and proceed to send the e ...

Testing an asynchronous function in JavaScript can lead to the error message: "Have you neglected to utilize await?"

Here is an example of the code I am working with: public getUrl(url) { //returns URL ... } public getResponseFromURL(): container { let myStatus = 4; const abc = http.get(url, (respon) => const { statusCode } = respon; myStatus = statusCode ...

The attribute 'split' is not found on the never data type

I have a function that can update a variable called `result`. If `result` is not a string, the function will stop. However, if it is a string, I then apply the `split()` method to the `result` string. This function always runs successfully without crashin ...

Unique Version: Some effective tips for utilizing a fork of type definition such as @types

Currently, I am utilizing Typescript 2.0 along with @types and the experience has been quite positive. Thanks to @types, we can easily leverage type definitions by simply installing the package via npm. Surprisingly, I have not delved into how it actually ...

Asserting types for promises with more than one possible return value

Struggling with type assertions when dealing with multiple promise return types? Check out this simplified code snippet: interface SimpleResponseType { key1: string }; interface SimpleResponseType2 { property1: string property2: number }; inter ...

A Guide to Implementing Inner CSS in Angular

I am working with an object named "Content" that has two properties: Content:{ html:string; css:string } My task is to render a div based on this object. I can easily render the html using the following code: <div [innnerHtml]="Content.html"& ...

How can we efficiently link data to custom objects (models) within a different class while fetching data from the server using the http.get() method in Angular CLI?

Currently in the process of developing an Angular-Cli application that involves multiple models with relational data tables. When fetching data from the server, I need to map this data to corresponding model objects. I've experimented with creating a ...

Having difficulties creating a new instance of a class

I'm encountering an issue with this TypeScript code import Conf = require('conf'); const config = new Conf(); The Problem: the expression is not constructable and the imported module lacks construct signatures It's puzzling because th ...

Exploring the possibilities of TypeScript/angularJS in HTTP GET requests

I am a beginner in typescript and angular.js, and I am facing difficulties with an http get request. I rely on DefinitelyTyped for angular's type definitions. This is what my controller code looks like: module game.Controller { 'use strict& ...

Next.js does not support tooltips with custom children components

I created a unique Tooltip component and I'm attempting to include NextLink as the children. However, I encountered an error similar to the one below. Warning: Failed prop type: Invalid prop `children` supplied to `ForwardRef(Tooltip)`. Expected an e ...

Tips for using conditional rendering with React and TypeScript

Issue with Conditional Rendering in TypeScript It seems like I might have encountered a problem with the way I declare my components. Take a look at this TypeScript snippet: import React, { FunctionComponent } from 'react'; export const Chapte ...