Crafting a nested path type in Typescript

Currently, I am working on developing a recursive type in TypeScript to iterate through every potential route path, even nested paths, from a provided route configuration. However, I have hit a roadblock with type constraints and inference.

The routes are defined using a TypedRoute interface and a createRoute function:

interface TypedRoute<
  Path extends string,
  QueryParams extends Record<string, string> = {}
> {
  path: Path;
  queryParams?: QueryParams;
  children?: TypedRoute<any, any>[];
}

function createRoute<
  Path extends string,
  QueryParams extends Record<string, string> = {}
>(config: TypedRoute<Path, QueryParams>) {
  return config;
}

const routes = [
  createRoute({
    path: 'results/:id/foo/:bar'
  }),
  createRoute({
    path: 'home',
    children: [
      createRoute({
        path: 'bar',
        children: [
          createRoute({ path: 'baz' })
        ]
      }),
    ],
  }),
]

My current task involves creating a type called ExtractRoutePaths that can recursively retrieve all feasible paths:

type ExtractRoutePaths<T extends readonly TypedRoute<any, any>[]> = 
  T extends readonly []
    ? never
    : T extends readonly [infer First, ...infer Rest]
      ? First extends TypedRoute<infer P, any>
        ? First extends { children: infer C }
          ? C extends readonly TypedRoute<any, any>[]
            ? `{P}` | ExtractRoutePaths<C, `${P}/`> | ExtractRoutePaths<Rest extends readonly TypedRoute<any, any>[] ? Rest : never>
            : `${P}` | ExtractRoutePaths<Rest extends readonly TypedRoute<any, any>[] ? Rest : never>
          : `${P}` | ExtractRoutePaths<Rest extends readonly TypedRoute<any, any>[] ? Rest : never>
        : ExtractRoutePaths<Rest extends readonly TypedRoute<any, any>[] ? Rest : never>
      : never;

I am aiming for the following Routes:

results/:id/foo/:bar | home | home/bar | home/bar/baz

If there are any errors in my approach, how can I modify this recursive type to correctly extract all possible paths, including nested ones?

Answer №1

In the discussion that follows, I am disregarding the queryParams details as they do not appear to be directly pertinent. You can reintegrate them into your own types, but for ExtractRoutePaths<T>, it is unnecessary. I will focus solely on the type

interface BaseRoute {
    path: string;
    children?: BaseRoute[]
}

This is all you need in order to extract the paths. The implementation of ExtractRoutePaths<T> only considers a single route; if multiple routes need to be processed, you can define the following

type ExtractRouteArrayPaths<T extends readonly BaseRoute[]> =
    ExtractRoutePaths<T[number]>;

Now, let's delve into ExtractRoutePaths<T>:

type ExtractRoutePaths<T extends BaseRoute> = T["path"] | (
    T extends { children: infer C extends readonly BaseRoute[] } ?
    `${T["path"]}/${ExtractRoutePaths<C[number]>}` : never
)

This represents a type of recursive conditional nature and is quite straightforward. It always includes T["path"] (the result when indexing a T object with the key "path"). This result is combined in a union with the recursive step. By utilizing infer and extends in a conditional type, we verify if T indeed has a property named children of the expected type (bearing in mind its optional status) and store this property in the variable C. If no such property exists, we reach the base case (never). Otherwise, we construct a template literal type by linking the current path with all child paths using slashes.

Note that the distributive property applies to the conditional type here, as well as to indexed accesses and template literal types. Therefore, the single type defined above naturally consolidates all distinct paths into a unified union.


To validate the functionality, we must provide a route type. However, before testing can be done, slight adjustments are required in your createRoute() function to accurately track the input data. Instead of inferring a wider type, specifying the entire type of the config parameter using generics is encouraged:

function createRoute<const T extends BaseRoute>(config: T) {
    return config;
}

It's worth noting the use of the const type parameter, which aims to derive narrow literal types especially for literal inputs rather than broader types like string.

With these changes in place, we can now proceed with testing:

const routes = [
    createRoute({
        path: 'results/:id/foo/:bar'
    }),
    createRoute({
        path: 'home',
        children: [
            createRoute({
                path: 'bar',
                children: [
                    createRoute({ path: 'baz' })
                ]
            }),
        ],
    }),
]

type Z = ExtractRouteArrayPaths<typeof routes>;
// type Z = "results/:id/foo/:bar" | "home" | "home/bar" | "home/bar/baz"

The output appears accurate. The provided type information within routes aptly represents the entire tree structure, making it possible for ExtractRouteArrayPaths (and consequently ExtractRoutePaths) to consolidating them into a union of respective paths.

It's worth mentioning that intricate recursive types such as this one may have peculiar edge cases and might require substantial revision to accommodate future scenarios. Thoroughly testing the type against various relevant cases is imperative, coupled with readiness to amend the type structure as needed in light of potential use cases or language modifications ahead.

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

What could be causing an error with NextJS's getStaticPaths when running next build?

When attempting to use Next.js's SSG with getStaticPaths and getStaticProps, everything worked fine in development. However, upon running the build command, an error was thrown: A required parameter (id) was not provided as a string in getStaticPath ...

Eliminate JSON data that pertains to dates that are either in the past or future

I am working on integrating upcoming classes and past classes components into my application. I have successfully stored the schedule of classes and can retrieve them using backend services. However, I need to display only the upcoming classes in one compo ...

Adding date restrictions to the main method is necessary for controlling the input and output

I have a function in my react-native package that requires "from" and "to" dates as parameters. I need to implement a rule that ensures the "to" date is always after the "from" date. Where should I insert this requirement in the following code snippe ...

Combining tuples with their corresponding data types

Trying to define a new type: type Union = [1, "one"] | [1, "first"] | [2, "two"] type GetTuple<T, U> = Extract<T, [U, ...unknown[]]>; type ObjectFromUnion<T extends Union[0] = Union[0]> = { number: T, word: ?? } Looking to utiliz ...

Displaying Modal from a separate component

CardComponent: export class Card extends Component<Prop, State> { state = { isCancelModalOpen: false, }; marketService = new MarketService(); deleteMarket = () => { this.marketService .deleteMar( ...

Navigating API data conversion on the frontend using Object-Oriented Programming

Currently, I am facing a challenge while developing the frontend of a web application using TypeScript. The dilemma revolves around efficiently converting a data object from an API response into a format suitable for the application. Let's consider r ...

Is there a way to specify object keys in alignment with a specific pattern that allows for a variety of different combinations

I am seeking a way to restrict an object to only contain keys that adhere to a specific pattern. The pattern I require is: "{integer}a+{integer}c". An example of how it would be structured is as follows: { "2a+1c": { // ... } } Is there a ...

Delete information based on its unique identifier

Currently, I am trying to remove data from a list but encountering a small issue with the following code. Component Code removeSelectedRows(){ console.log(this.selection.selected.map(item => item.userId)) const selectedRowIds = this.selection. ...

Check if a value is present in the array with *ngIf

I'm curious about how to use the ngIf directive in a specific scenario. In my Angular application, I have dynamically generated angular material toggles, each with a unique id. I'm familiar with using ngIf to conditionally display elements on the ...

Exploring Iteration of TypeScript Arrays in ReactJS

Issue Encountered: An error occurred stating that 'void' cannot be assigned to type 'ReactNode'.ts(2322) The error is originating from index.d.ts(1354, 9) where the expected type is related to the property 'children' defi ...

The ESLint setup specified in the package.json file for eslint-config-react-app is deemed to be incorrect

The property named "overrides" has the incorrect type (expected array but received {"files":["**/*.ts","**/*.tsx"],"parser":"@typescript-eslint/parser","parserOptions":{"ecmaVersion":2018,"sourceType":"module","ecmaFeatures":{"jsx":true},"warnOnUnsupported ...

Is there a way to eliminate text from a barcode image using JavaScript or TypeScript?

This is my unique html code <div class="pr-2" style="width: 130px"> <div *ngIf="!element.editing" > <span class="ss">{{element.barcode}}</span> </di ...

What is the best way to pinpoint a specific type from multiple derived class instances when working with an array?

When working inside a .forEach loop, I am encountering an issue with narrowing down a type (the last statement in the snippet below). Within that loop, TypeScript interprets that obj is either of type ObjA | ObjB (since TypeScript created a union of all p ...

Is there a method to create a typecheck for hasOwnProperty?

Given a certain interface interface Bar { bar?: string } Is there a way to make the hasOwnProperty method check the property against the defined interface? const b: Bar = { bar: 'b' } b.hasOwnProperty('bar') // works as expected b. ...

Improving type checking by extracting constant string values from a union type

I am exploring different types of employees: interface Employee { employeeType: string } interface Manager extends Employee { employeeType: 'MANAGER' // .. etc } interface Developer extends Employee { employeeType: 'DEVELOPER&apos ...

Creating a function in TypeScript that returns a string containing a newline character

My goal is to create a function that outputs the text "Hello" followed by "World". However, my current implementation does not seem to be working as expected. export function HelloWorld():string{ return "Hello"+ "\n"+ "W ...

Develop a FormGroup through the implementation of a reusable component structure

I am in need of creating multiple FormGroups with the same definition. To achieve this, I have set up a constant variable with the following structure: export const predefinedFormGroup = { 'field1': new FormControl(null, [Validators.required]) ...

Combining multiple Observables and storing them in an array using TypeScript

I am working with two observables in my TypeScript code: The first observable is called ob_oj, and the second one is named ob_oj2. To combine these two observables, I use the following code: Observable.concat(ob_oj, ob_oj2).subscribe(res => { this.de ...

Determine the data type of the value for the mapped type

Is there a way to access the value of a type like the following: interface ParsedQs { [key: string]: undefined | string | string[] | ParsedQs | ParsedQs[] } I am looking for a method to retrieve the ParsedQsValue type without directly duplicating it from ...

When running the command ng build --prod, it seems that the module for class X cannot be determined. To resolve this issue, make sure to include

I'm encountering an issue while trying to develop my Angular 5 application. The error message reads: Cannot determine the module for class ThreadListTabsComponent in /home/brightwater/Differ/src/app/thread-lists/thread-lists.component.ts! Add T ...