Create a compilation of categories/interfaces based on a mapping

Imagine you have the following object:

const ROUTES = {
  PAGE_NO_PARAMS: '/hello/page/two',
  PAGE_R: '/about/:id',
  PAGE_Z: '/page/page/:param/:id',
  PAGE_N: '/who/:x/:y/:z/page',
} as const

Can we create a set of types/interfaces for each route so that developers are limited to using only valid parameters for the selected route?

In other words, can we generate a type from ROUTES that behaves in a similar way to the type RouteAndParams shown below?

interface PageNoParams = {
   route: '/hello/page/two'  // no params
}

interface Param1 = {
   route: '/about/:id',
   params: { id: string } // required params
}

interface PAGE_Z = {
   route: '/page/page/:param/:id',
   params: { id: string; param: string } // required params
}

interface Param3 = {
   route: '/who/:x/:y/:z/page',
   params: { x: string; y: string; z: string } // required params
}

type RouteAndParams = PageNoParams | Param1 | PAGE_Z | Param3;

// Examples of expected results / errors when using the generated type

// Should NOT error
const routeWithParams: RouteAndParams = {
   route: '/page/page/:param/:id',
   params: { 'param': 'blah', 'id': 'xxx' }
}

// Should error due to unexpected param 'x'
const routeWithParams: RouteAndParams = {
   route: '/about/:id',
   params: { 'id': 'xxx', 'x': 'xxx' }
}

// Should error as param 'y' is missing
const routeWithParams: RouteAndParams = {
   route: '/who/:x/:y/:z/page',
   params: { 'x': 'blah', 'z': 'blah' }
}

This approach aims to catch potential errors during build time instead of runtime.

Answer №1

To efficiently process the path and extract parameters, recursive conditional types can be utilized:


type Routes = MakeValidRoute<typeof ROUTES[keyof typeof ROUTES]>
 // = "/hello/page/two" | `/about/${string}` | `/page/page/${string}/${string}` | `/who/${string}/${string}/${string}/page`
 
type GetInterfaceKeys<T extends string, R extends string = never> =
  T extends `${string}/:${infer Name}/${infer Tail}`?
    GetInterfaceKeys<`/${Tail}`, R | Name>:
  T extends `${string}/:${infer Name}`?
    R | Name:
    R

type MakeRouteAndParams<T extends Record<string, string>>  = {
  [P in keyof T]: {
    route: T[P],
    params: Record<GetInterfaceKeys<T[P]>, string>
  }
}[keyof T]

type RouteAndParams = MakeRouteAndParams<typeof ROUTES>
// type RouteAndParams = {
//     route: "/hello/page/two";
//     params: Record<never, string>;
// } | {
//     route: "/about/:id";
//     params: Record<"id", string>;
// } | {
//     route: "/page/page/:param/:id";
//     params: Record<"id" | "param", string>;
// } | {
//     ...;
// }

Complete Refactoring Available Here!

Enhancing compiler performance by implementing tail recursive conditional types.

Answer №2

Here is a unique solution:

type ParameterNames<T extends string, A extends string = never> =
  T extends `${infer F}:${infer E}/${infer R}` ?
  ParameterNames<R, A | E> : T extends `${infer F}:${infer E}` ?
  A | E : A    

ParameterNames<T> represents a recursive conditional type that converts a string containing colon-delimited path parameters into a union of those parameter names. For example:

type Example = ParameterNames<"/one/:two/three/:four">
// type Example = "two" | "four"

You can then use another conditional type to create your route-and-parameters pairs:

type RouteAndParams<T extends string> = T extends unknown ?
  ParameterNames<T> extends infer S extends string ?
  [S] extends [never] ?
  { route: T } :
  { route: T, params: { [K in S]: string } }
  : never : never

This structure handles scenarios where the params property should be suppressed if ParameterNames<T> resolves to never. Let's test it out:

const ROUTES = {
  PAGE_NO_PARAMS: '/example/page/two',
  PAGE_R: '/info/:id',
  PAGE_Z: '/page/page/:param/:id',
  PAGE_N: '/who/:x/:y/:z/page',
} as const

type RP = RouteAndParams<typeof ROUTES[keyof typeof ROUTES]>
/* Output:
   type RP = {
     route: "/example/page/two";
   } | {
     route: "/info/:id";
     params: {
       id: string;
     };
   } | {
     route: "/page/page/:param/:id";
     params: {
       id: string;
       param: string;
     };
   } | {
     route: "/who/:x/:y/:z/page";
     params: {
       x: string;
       y: string;
       z: string;
     };
   }
*/

It appears to be functioning correctly.

Link to code on TypeScript 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

``Incorporating event and parameter as arguments for a function in an Angular application: a tutorial

I am trying to incorporate a checkbox in Angular where if it is enabled, the event.target.checked value is true, and if it is disabled, the event.target.checked value is false. When clicking the checkbox, I need to call a function where I want to pass the ...

What is the reason that setState functions properly when parsing each key separately, but fails when passed as an object?

Currently, I am delving into the world of React and TypeScript, but I have encountered a problem when trying to pass an object with a specific type in order to update the state. For some reason, the state remains unchanged. My approach involves using the ...

Troubleshooting a Gulp.js issue during the execution of a "compile" task

Currently, I am utilizing gulp to streamline a typescript build system specifically designed for an Angular 2 frontend. However, I have encountered a problem with my "build" task that has been configured. Below is the exact task in question: gulp.task(&a ...

Error: Can't find module ng-uikit-pro-standard

I am currently working on implementing datatables in Angular with material design. To achieve this, I am referencing a tutorial from this source. The tutorial instructs to import the MdbTableDirective, MdbTablePaginationComponent, and MdbTableService from ...

Removing Angular Template space highlights in WebStorm can be done easily with a few simple steps

Is there a way to remove space highlights in Angular / TypeScript using WebStorm 2019? https://i.stack.imgur.com/vfudR.jpg Many thanks, Sean ...

The connection between Parent and Child components within the Angular framework

Can changes made in a Child component automatically reflect in the Parent component when passing variables from parent to child? If we send any variable from parent to child and then make changes in the Child component, will these changes be automatica ...

Transform a javascript object with class attributes into a simple object while keeping the methods

I am seeking a way to convert an instance of a class into a plain object, while retaining both methods and inherited properties. Here is an example scenario: class Human { height: number; weight: number; constructor() { this.height = 1 ...

Guide to adding jquery with typings installation

Need assistance: typings install jquery --global typings ERR! message Unable to find "jquery" ("npm") in the registry. Did you want to try searching another source? Also, if you want contribute these typings, please help us: https://github.com/typings/re ...

There seems to be an issue with gulp as it is not functioning properly and the version information is

Currently, I am working on a project and have made the decision to utilize gulp for watching and transpiling Typescript files. Below are the steps I followed to set everything up: All of these actions were performed within the main directory of my projec ...

Encountering the error "tsx is not defined" during a Jest test in a React/TypeScript project

I'm currently working on implementing Jest tests within a React project that has enforced TypeScript settings. In a simple test.tsx file located in the test folder, I have the following code: import React from 'react'; describe('Test& ...

Tips on ensuring that only one Angular Material expansion panel expands at a time

I have designed a mat expansion panel and I would like to ensure that only one panel can be expanded at a time. In other words, I want it so that when one record is expanded and I click on another record of the mat expansion, the previously expanded reco ...

Incorporate additional fields into the info.plist file for a Cordova iOS application

I am in the process of developing a custom plugin that will automatically add entries to the info.plist file for an iOS application built with Cordova and Angular 4. One specific entry I need to include triggers the application to exit when the home button ...

Encountered Angular SSR Serve Error: NullInjectorError - StaticInjectorError in AppServerModule with the following reference:

While working on building an application with Angular's SSR and serving it, I encountered a specific error. All services and components have been properly injected. Error: ERROR Error [NullInjectorError]: StaticInjectorError(AppServerModule)[REQUEST] ...

The onRowSelect and onRowClick events are not being triggered on the Primeng table component in an Angular application

I am currently struggling to navigate to another component with the data selected when a row is clicked. I have been using p-table to accomplish this task. For some reason, neither onRowClick nor onRowSelection functions are being triggered. I even added ...

angular-cli: Select templates based on the current environment

Currently, I am utilizing @angular/cli: 1.0.0 and aiming to utilize component templates based on the environment. The code implementation is as follows: import {Component} from '@angular/core'; import {environment} from '../environments/env ...

"Encountered an ENOENT error message following the deployment

I'm really hoping for some insight into the current situation. Deploying an Angular 7 / .Net Core 2 application to Azure is giving me trouble. I am utilizing the publish profile provided by Azure in Visual Studio. Everything runs smoothly when testi ...

The panel header is clickable and overlaps with the header buttons

My panel component includes a header with a title and buttons positioned in the right corner. Currently, the downward arrow (chevron) is used to toggle the expansion/minimization of the panel's contents. When I attempt to make the header clickable to ...

Having trouble importing Angular flex-layout into my feature module

I'm facing an issue with Angular flex-layout in one of my modules. It works perfectly fine when I import flex-layout in the app module, but only for the app component. However, when I import flex-layout in another module, it doesn't seem to work ...

Expanding a TypeScript interface across different modules

For my project, I am working with Highcharts typings and encountered a need to extend certain object/interfaces it defines by adding some custom properties. Here is an example: declare namespace Chart { interface ChartOptions extends Highcharts.ChartOpt ...

Incorporating a Component with lazy-loading capabilities into the HTML of another Component in Angular 2+

Striving to incorporate lazy loading in Angular 2, I have successfully implemented lazy loading by following this helpful guide. Within my application, I have two components - home1 and home2. Home1 showcases the top news section, while home2 is dedicated ...