Create a combined string from a structure resembling a tree

In my web application, I have a type that defines the different routes available:

interface IRoute {
  readonly path: string,
  readonly children?: IRoute[]
}

I want to create a union type containing all possible paths based on an IRoute object.

Let's consider this example data:

const routes: IRoute = {
  path: '',
  children: [
    {
      path: '/dashboard',
    },
    {
      path: '/settings',
      children: [
        {
          path: '/notifications'
        },
        {
          path: '/profile',
          children: [
            {
              path: '/picture'
            },
          ]
        }
      ]
    }
  ]
};

Is it feasible to generate a union type representing all the possible routes given an IRoute object?

type AllRoutes = 
 | '/'
 | '/dashboard'
 | '/settings'
 | '/settings/notifications'
 | '/settings/profile'
 | '/settings/profile/picture'

Playground Link

Answer №1

In order to proceed with writing a function of type IRouteToPaths<T>, we have some adjustments to make first.

type AllRoutes = IRouteToPaths<typeof routes>;

This function will generate a union of paths as shown below:

/* type AllRoutes = 
   "" | 
   "/dashboard" | 
   "/settings" | 
   "/settings/notifications" | 
   "/settings/profile" | 
   "/settings/profile/picture" 
*/

If you explicitly specify the type of routes as IRoute, the compiler will only recognize routes based on that information, losing any specific details about the initializer and hindering the extraction of a path union from routes:

const routes: IRoute = { ... }

type RootPath = typeof routes.path;
// type RootPath = string // 🤷‍♂️
type RootChildren = typeof routes.children;

// type RootChildren = IRoute[] | undefined // 🤷‍♀️

It would be best to let the compiler infer the type of routes, and then verify it against IRoute afterwards if necessary, potentially utilizing the new satisfies operator:

const routes = { ... } satisfies IRoute;

Moreover, an object like

{path: children: [{path: "a"}, {path: "b"}]
may get inferred as type
{path: string, children: {path: string}[]}
, which contains more data than IRoute, but still lacks enough for extracting the union of paths. To retain the literal types of the path properties and subproperties, employ a const assertion:

const routes = { ... } as const satisfies IRoute;

Additionally, using const assertions results in inferring readonly tuple types. To avoid complications, adjust IRoute so that the children property can also be readonly:

interface IRoute {
  readonly path: string,
  readonly children?: readonly IRoute[] 
}

We are now ready to define IRouteToPaths. Here is one possible implementation:

type IRouteToPaths<T extends IRoute> =
  T extends IRoute ? `${T['path']}${"" | (
    T['children'] extends readonly (infer R extends IRoute)[] ? IRouteToPaths<R> : never
  )}` : never

This is a recursive conditional type, handling base and recursive cases depending on the presence of children.

As a distributive conditional type,

IRouteToPaths<T1 | T2 | T3>
equals to
IRouteToPaths<T1> | IRouteToPaths<T2> | IRouteToPaths<T3>
. This property comes in handy when recursing into children, allowing direct processing of each element's union through IRouteToPaths.

The function indexes into the "path" property of T, returning a template literal type assembling paths and child unions.


Verification time. Let's apply the code:

const routes = {
  path: '', children: [
    { path: '/dashboard' },
    {
      path: '/settings', children: [
        { path: '/notifications' },
        {
          path: '/profile', children: [
            { path: '/picture' },
          ]
        }
      ]
    }
  ]
} as const satisfies IRoute;

type AllRoutes = IRouteToPaths<typeof routes>
/* type AllRoutes = 
   "" | 
   "/dashboard" | 
   "/settings" | 
   "/settings/notifications" | 
   "/settings/profile" | 
   "/settings/profile/picture" 
*/

All seems well.

View code in Playground

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

Find the identifier that does not currently exist in the collection of objects

There is a situation where I have an array and an object that consists of arrays of ids, which are essentially permission objects. My goal now is to extract the ids that do not exist in the given object. Can someone assist me with devising the necessary l ...

Switch between classes when hovering over / exiting ngFor elements

Displayed below is an element created using ngFor <span *ngFor="let picture of pictures; let i = index"> <a target="_blank" href="{{picture.image}}" class="thumbnail-display image-overlay"> <span class="overlay-icon hide"> ...

What is the best way to execute an Nx executor function using Node.js?

Can a customized Nx executor function be executed after the app image is built? This is the approach I am taking: "migrate-up": { "executor": "@nx-mongo-migrate/mongo-migrate:up", "options": { &q ...

Problems with installing ambient typings

I'm having trouble installing some ambient typings on my machine. After upgrading node, it seems like the typings are no longer being found in the dt location. Here is the error message I am encountering: ~/w/r/c/src (master ⚡☡) typings search mo ...

Updating ComboBox Selection in Angular 4

When attempting to populate a combobox with the value from a selected row, only the inputs are loading. This section is part of my page: ` <form class="form-horizontal form-label-left parsleyjs" method="post" data-parsley-priority-enabled="false" n ...

Angular 12's BehaviorSubject should have been empty object array, but it unexpectedly returns undefined

Exploring different scenarios with a livesearch functionality: No user input (observable = undefined) No results found (observable = empty array) Results found (observable = non-empty array) The issue lies in how my behavior subject interprets an empty a ...

Error message: Issue with AWS Serverless Lambda and Angular - TypeError: express function not defined

I'm encountering an issue when trying to deploy my application from localhost:4200 to AWS serverless Lambda. The error in cloudwatch logs is causing a 500 {"message": "Internal server error"} response when I access the URL. My understanding of AWS is ...

Is it necessary for a TypeScript Library's repository to include the JavaScript version?

Is it necessary to include a JavaScript version of the library along with the Typescript repository for consumers? Or is it best to let consumers handle the compilation process themselves? Or should I consider another approach altogether? ...

Saving a local JSON file in Angular 5 using Typescript

I am currently working on developing a local app for personal use, and I want to store all data locally in JSON format. I have created a Posts Interface and an array with the following data structure: this.p = [{ posts:{ id: 'hey man&ap ...

Crafting a Retro Style

I have an interface called Product which includes properties such as name, and I want to track changes to these products using a separate interface called Change. A Change should include the original Product as well as all of its properties prefixed with t ...

Exploring methods for interacting with and controlling structural directives in e2e testing

Background: My goal is to permutation all potential configurations of an Angular2 screen for a specified route and capture screenshots using Protractor from the following link: http://www.protractortest.org/#/debugging. Problem: I am struggling to figure ...

Launching React Native in Visual Studio Code: launchReactNative.js

Struggling to get visual studio code to create the launchReactNative.js file in the ./vscode directory. I've been attempting to configure a react-native project with typescript on visual studio code to debug typescript files, but all my efforts have ...

The element of type 'OverridableComponent<LinkTypeMap<{}, "a">>' cannot be assigned to a 'ReactNode'

I'm currently working on a project where there's a component named ListItemTextStyle. Within that component, the prop type is defined as follows: import { LinkProps, ListItemButtonProps, } from '@mui/material'; type IProps = LinkP ...

Issues with Tagged Union Types in Visual Studio Code

Currently, I am working on implementing a tagged union type pattern for my action creators within a redux application. The TypeScript compiles without any issues, however, my code editor, Visual Studio Code 1.26.1, is flagging an error. [ts] Type &ap ...

Unable to delete event listeners from the browser's Document Object Model

Issue at hand involves two methods; one for initializing event listeners and the other for deleting them. Upon deletion, successful messages in the console confirm removal from the component's listener array. However, post-deletion, interactions with ...

Convert a Java library to JavaScript using JSweet and integrate it into an Angular project

Just recently, I embarked on my journey to learn TypeScript. To challenge my newfound knowledge, I was tasked with transpiling a Java library using JSweet in order to integrate it into an Angular project. This specific Java library is self-contained, consi ...

An issue has occurred: Unable to locate a supporting object 'No result' of type 'string'. NgFor is only compatible with binding to Iterables like Arrays

I am attempting to utilize this code to post data from a web service. service.ts public events(id: string): Observable<Events> { ...... return this.http.post(Api.getUrl(Api.URLS.events), body, { headers: headers }) .map((re ...

Configuring the React Typescript router to support username-based URLs should be done in a way that does not cause conflicts with other routes

I am looking to display a user's profile on a URL format such as www.domain.com/<userName> and load the component ShowProfile. I want to ensure that terms is not mistaken for a username, so if I visit www.domain.com/terms, I do not want to load ...

Testing server sent events with Angular solely using Karma-Jasmine

I am currently developing a web application using Angular for the frontend and Python for the backend. My implementation involves utilizing server-sent events (SSE) to stream data from the server to the user interface. While everything is functioning prope ...

Top method for allowing non-component functions to update Redux state without the need to pass store.dispatch() as a parameter

As I work on my first ReactJS/redux project, I find myself in need of some assistance. I've developed a generic apiFetch<T>(method, params) : Promise<T> function located in api/apiClient.ts. (Although not a React component, it is indirect ...