Steer clear of duplicating template literal type entries when dealing with optional routes

My code includes a type called ExtractParams that extracts parameters from a URL string:

type ExtractParams<Path extends string> = Path extends `${infer Start}(${infer Rest})`
  ? ExtractParams<Start> & Partial<ExtractParams<Rest>>
  : Path extends `${infer Start}/:${infer Param}/${infer Rest}`
  ? ExtractParams<Start> & ExtractParams<Rest> & { [Key in Param]: string }
  : Path extends `${infer Start}/:${infer Param}`
  ? ExtractParams<Start> & { [Key in Param]: string }
  : {};

The purpose of the ExtractParams type is to convert dynamic route parameters into an object with the parameter names as keys and string values. If a route parameter is optional, the generated object will reflect this by marking that key as optional with a value of string | undefined.

Here are some examples of using the type:

  type RP1 = ExtractRouteParams<'/courses/:courseId/classes/:classId'>;
  //   ^? { courseId: string; } & { classId: string }
  type RP2 = ExtractRouteParams<'/courses/:courseId/classes(/:classId)'>;
  //   ^? { courseId: string; } & { classId?: string | undefined }

To make the resulting object type cleaner and easier to read, I used a utility type obtained from this question, which merges the intersection of object types:

type Expand<T> = T extends infer U ? { [K in keyof U]: U[K] } : never;

By applying the Expand utility, I was able to improve the readability of the type:

type Params<Path extends string> = Expand<ExtractParams<Path>>;

type X1 = Params<'/courses/:courseId/classes/:classId'>
//   ^? { classId: string; courseId: string }
type X2 = Params<'/courses/:courseId/classes(/:classId)'>
//   ^? { classId?: string | undefined; courseId: string }

In summary, the code functions correctly when defining optional parameters in the format a(/:b).

I am looking to minimize repetition in the type declaration and focus on the syntax for declaring optional params as a(/:b). If there is a solution that accommodates multiple optional param syntaxes, it would be beneficial for future use.

For my specific use case, paths can have multiple optional parameters but will always be separated by at least one required parameter. Even if a solution allows for multiple optional parameters consecutively, it will not impact me negatively.

Valid examples of paths containing optional parameters include:

'/courses(/:courseId)/classes/:classId' - courseId is optional
'/courses/:courseId/classes(/:classId)' - classId is optional
'/courses(/:courseId)/classes(/:classId)' - courseId and classId both are optional
'/courses(/:courseId)(/:classes)(/:classId)' - Additional scenarios are welcomed, but not mandatory.

Invalid examples that I am certain won't be present in my codebase include paths like these:

'(/courses/:courseId)/classes/:classId' - Optional params should not have two slashes
'/courses(/:courseId/classes)/:classId'

Feel free to explore the Playground Link

Answer №1

A custom utility type can be created to extract elements from a string literal type that are located inside and outside of parentheses. This utility, named ReqandOptPieces<T>, returns an object type with the properties {Rq: ⋯, Op: ⋯}. Here, Rq represents the chunks outside parentheses and Op represents the chunks inside parentheses:

type ReqAndOptPieces<T extends string,
  Rq extends string = never, Op extends string = never>
  = T extends `${infer L}(${infer M})${infer R}` ?
  ReqAndOptPieces<R, Rq | L, Op | M> :
  { Rq: Rq | T, Op: Op }

This is achieved through tail recursive conditional types utilizing template literal types for splitting based on parentheses. Examples provided illustrate its usage.


Another utility type, PathSegments<T>, splits each path segment into individual components:

type PathSegments<T extends string, A extends string = never> =
  T extends `${infer L}/${infer R}` ? PathSegments<R, A | L> : A | T;

The above utility separates a given string into distinct path segments effectively.


Combining these utilities produces ReqAndOptSegments<T> which further refines extracted fragments:

type ReqAndOptSegments<T extends string> = ReqAndOptPieces<T> extends 
  { Rq: infer Rq extends string, Op: infer Op extends string } ?
  { Rq: PathSegments<Rq>, Op: PathSegments<Op> } : never;

It filters strings at the beginning assigned with ":" and then removes it in one step using SegmentToParam<T>.

Merging all previous steps leads to a composite utility type called Params<T>:

type Params<T extends string,
  Rq extends string = never, Op extends string = never>
  = T extends `${infer L}(${infer M})${infer R}` ? Params<R, Rq | L, Op | M> :
  { [K in keyof (
    Record<SegmentToParam<<PathSegments<Rq | T>>, 0> &
    Partial<Record<SegmentToParam<PathSegments<Op>, 0>>
  )]: string}

The final iteration combines all operations into a single comprehensive utility type producing expected results when tested.

[Playground link](https://www.typescriptlang.org/play?#code=FD...)

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

Need help with creating a unit test for the Material UI slider component? An error message saying "Error: Cannot read property 'addEventListener' of null" is displayed when trying to render the component

Encountered a problem while testing the Material-UI Slider with React-Test-Renderer: Uncaught [TypeError: Cannot read property 'addEventListener' of null] Codesandbox Link import React from "react"; import { Slider } from "@materi ...

Jsx Component fails to display following conditional evaluations

One issue I am facing is having two separate redux stores: items (Items Store) quotationItems (Quote Items). Whenever a product item is added to quotationItems, I want to display <RedButton title="Remove" />. If the quotationItems store i ...

Parsing error occurred: Unexpected empty character found while attempting to load .lottie files

I have a NextJS application and I'm integrating the dotLottie player from this repository. Even though I've followed the setup instructions provided in the documentation, I keep encountering an error when the component attempts to load the dotLot ...

What is the best way to form a new type that encompasses all shared properties found within a union of types?

Is there a method to safely map over the union of arrays without hard-coding specific types? When attempting to calculate newArr1, an error is encountered: Property 'field2' does not exist on type 'Common<A, B>'. How can this err ...

Ensure to call the typescript file every time the page is reloaded or when a URL change occurs

Looking to integrate a session feature into my Angular 5 application. I aim to create a single TypeScript file that will handle user login validation. Is there a way to trigger this file every time the page reloads or the URL changes? Need guidance on im ...

Animating the Click Event to Change Grid Layout in React

Can a grid layout change be animated on click in React? For instance, consider the following component: import { Box, Button, styled, useMediaQuery } from "@mui/material"; import Row1 from "./Row1"; import React from "react"; ...

Odd behavior of escape characters in Typescript

Looking for help with a query similar to the one referenced here. I am new to TypeScript and front end development. Currently using an Angular form to collect user input, which may contain regex. For example: The input from the form, stored in this.expr ...

What causes TS2322 to only appear in specific situations for me?

I have been trying to create HTML documentation for my TypeScript project using Typedoc. Within one of the many files, there is a snippet of code: public doSomething(val: number | undefined | null | string): string | undefined | null { if (val === null ...

How can you properly structure chainable functions in Angular?

Recently, I've been working on developing custom functions for my Angular application. Following the official guidelines, I have created an independent library. My goal is to create chainable functions similar to this: var obj = { test : function( ...

Defining the TypeScript interface for the onClick event in ReactJS

If you're looking to dive into React development, the tutorial on reactjs.org is a great place to start. While the tutorial code is in JavaScript, I've been working on converting it to TypeScript. I've successfully translated most of the c ...

Generics causing mismatch in data types

I decided to create a Discord bot using DiscordJS and TypeScript. To simplify the process of adding components to Discord messages, I developed an abstract class called componentprototype. Here is how it looks (Please note that Generators are subclasses li ...

Is there a way for React to recognize index.ts as the root file of a folder?

I recently started working on a new React project and I'm facing an issue with resolving the index.js file as the folder being imported in another component. Expected outcome: No errors // src/pages/router.tsx import HomePage from './home-page` ...

Loading custom components dynamically in Angular with SVG: a how-to guide

Looking for a way to dynamically load SVG items with ease. The items needed are quite simple. Here's a basic template: <svg:rect [attr.x]="x" [attr.y]="y" width="10" height="10" /> Component Class Example: export class DraggableSvgItemCompon ...

Having trouble making generics work with extends in Typescript

I am facing an issue when trying to limit input to an object, but unfortunately, it is not working correctly: displayModal<T extends {[key: string]: any}, U>(component: Type<AbstractDialogComponent<T, U>>, options?: ModalDialogOption ...

Determining When to Activate Button Based on Angular - Verifying That All Choices Have Been Ch

This quiz application requires the user to choose options before proceeding to the next page, with the next button being disabled by default. Once all options are chosen, the next button should become enabled. NOTE: Although the functionality for selecti ...

"Utilizing generic types with the 'extends' keyword to input arguments into a function that requires a more specific

I recently tried out the TypeScript playground and came across a puzzling issue that I can't seem to wrap my head around. Below is the code snippet: type Foo = { t: string; } type Bar = string | { date: Date; list: string; } function te ...

Can the Rxjs library's Observables of() function be used to output multiple values?

I am inquiring about this because I came across in the Angular documentation that of(HEROES) returns an Observable<Hero[]> which emits a single value - an array of mock heroes. If I cannot use of(), do you have any alternative suggestions for me? I ...

The compiler is showing an error with code TS5023, indicating that the option 'strictTemplates' is not recognized

When trying to compile my Angular application (v10), I encountered the following error message. An unexpected issue has occurred: tsconfig.json:14:5 - error TS5023: Unknown compiler option 'strictTemplates'. 14 "strictTemplates": t ...

How can you create an interface where the value type is determined by the key, but not all keys are predefined?

Below is an illustration of a data structure that I aim to define as a type in TypeScript: const dataExample = { Folder: { "Filename.js": { lines: { total: 100, covered: 80, ...

Navigating in express

Here is the structure I am working with: server.ts routes/ index.ts homeRoute.ts In server.ts: let app = Express(); app.use(router); In routes/index.ts: const routes = Router(); export default function router() { routes.use('/home' ...