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

Combine the remaining bars by stacking the highest one on top in Highchart

Making use of stacking to display the highest value as the longest column/bar, with smaller values being merged within the highest one, can create a more visually appealing stack chart. For example, when looking at Arsenal with values of 14 and 3, ideally ...

What is the best way to set the typing of a parent class to the child constructor?

I am seeking a method to inherit the parameter types of a parent's constructor into the child's constructor. For example: class B extends A { constructor (input) { super(input); } } I attempted the following: class B extends ...

Utilizing Angular's ngx-bootstrap date range picker for a customized date range filtering system

Currently, I am incorporating ngx-bootstrap's datepicker functionality and utilizing the date range picker. This feature allows users to choose a start and end date. After selecting these dates, my goal is to filter the array(content) based on whethe ...

What are the steps to implementing MSAL in a React application?

Struggling to integrate the msal.js library with react. Post Microsoft login, redirecting to callback URL with code in the query string: http://localhost:3000/authcallback#code=0.AQsAuJTIrioCF0ambVF28BQibk37J9vZQ05FkNq4OB...etc The interaction.status re ...

What is the best way to bring a string into my .tsx file using a relative reference from a module?

I am currently developing an online course on creating a website using StencilJS, NodeJS, and the IonicFramwork. As a newcomer in this field, I have encountered a challenging issue: In my project, the API "https://swapi.dev/api" is imported as a ...

Send a collection of objects by submitting a form

I have a component with the following html code. I am attempting to dynamically generate a form based on the number of selected elements, which can range from 0 to N. <form #form="ngForm" id="formGroupExampleInput"> <div class="col-xs-5 col-md- ...

Using Angular to pass an index to a pipe function

Currently, I am attempting to incorporate the *ngFor index into my pipe in the following manner: <td *ngFor="let course of courses | matchesTime:time | matchesWeekday:i ; index as i">{{course.courseName}}</td> This is how my pipe is structure ...

Create Office Script calculations with quotations included

I am currently attempting to create an Excel office script formula for a cell. Are there any tips on how to insert a formula with quotes into a cell? For example, the following works fine: wsWa.getCell(WaRangeRowCount, 9).setFormula("= 1 + 1"); ...

Implementing a feature in ReactJS that allows users to upload multiple images in base64 format

I'm trying to develop an image uploader using base64 and I want the output as an array. However, I am encountering an issue where the array is coming out empty!. I suspect it might be due to an asynchronous problem. Any tips on how to incorporate asyn ...

Executing multiple http post requests in Angular2 using a for loop

I've encountered an issue while attempting to upload multiple files with individual titles. The problem arises when sending requests to the server, as I'm trying to pass each file and its corresponding title one by one. I have an array called bin ...

When trying to set the focus on the first item in a list using HTML and Angular, the focus unexpectedly shifts to the second

I've been tackling a UI requirement where the focus needs to be set on the first element of a list item constructed from an array of objects when the tab key is pressed for the first time. Subsequent tab key presses should cycle through the list items ...

Tips for having tsc Resolve Absolute Paths in Module Imports with baseUrl Setting

In a typescript project, imagine the following organizational structure: | package.json | tsconfig.json | \---src | app.ts | \---foobar Foo.ts Bar.ts The tsconfig.json file is set up t ...

Enroll a nearby variable "Data" to an Observable belonging to a different Component within an Angular application

Looking to update the HTML view using *ngIf, depending on a local variable that should change based on an observable variable from a shared service. HTML <div class="login-container" *ngIf="!isAuthenticated"> TypeScript code for the same componen ...

what is the best way to eliminate comments from nested arrays when using useReducer?

Can someone help me figure out how to use useReducer and useContext to manipulate global state? I specifically need to know how to delete comments using useReducer. Data Structures View the interface image here Sample Data Array export const listsData:IDa ...

Transforming an array into an object using TypeScript

I am attempting to create a generic type for a function that transforms an array into an object, like so: type ObjectType = { id: number; name: string; status: string }; const xyz: ObjectType[] = [ { id: 1, name: "X", status: " ...

Handling click events on Datatable.net paging buttons

My goal is to capture the click event when one of the paging buttons on the Datatable is clicked in Angular. I'm not exactly sure how to go about accomplishing this! If I refer to this example, how can I adapt the code for Angular? Specifically, how ...

A guide to incorporating Material-UI ThemeProvider and WithStyles with Typescript

I've been feeling frustrated lately as I've been dedicating the past few days to migrating my React application from JavaScript to TSX. While I appreciate the type checking that TSX provides, I'm struggling with understanding how to implemen ...

Prisma and Next.js: Changes to content require re-deployment for updates to take effect

Just recently, I launched a new website on Vercel. My web application is being built with Prisma and Next.js. However, I'm currently facing an issue where the content doesn't update in real-time unless I manually re-deploy the application. Here&a ...

Determine whether a response is not received within 8 seconds

One of the methods in my Angular component is responsible for returning data Here is a snippet of that method getRecognitionById() { this.loaderService.show(null, true); forkJoin( this.vendorWebApiService.getRecognitionById(this.executiveCh ...

What is a more efficient method for incorporating optional values into an object?

Currently, I am utilizing the optional addition feature in this way: ...(!!providerId && { providerId }), ...(!!practiceId && { practiceId }), Is there a more elegant shorthand method to replace this logic, such as: yield createRemark ...