Relationship requirement between two attributes of an entity

My goal is to enhance the types for a frontend route configuration object by linking the `path` and `prepare` fields together. This way, the `prepare` function field will receive the appropriate parameters based on the `:keys` in the path.

Each route item in the configuration (other fields excluded for brevity) appears as follows:

interface Routes<Path extends string> {
    path: Path;
    prepare?: (params: PathArgs<Path>) => object;
    routes?: Routes[];
}

The utilities used to extract the parameters from the path string are

type PathParams<
  Path extends string
> = Path extends `:${infer Param}/${infer Rest}`
  ? Param | PathParams<Rest>
  : Path extends `:${infer Param}`
  ? Param
  : Path extends `${infer _Prefix}:${infer Rest}`
  ? PathParams<`:${Rest}`>
  : never;

type PathArgs<Path extends string> = { [K in PathParams<Path>]: string };

// { siteId: string }
type x = PathArgs<`/dashboard/sites/:siteId`>

Ideally, if I define something like

const routes: Routes[] = [
  {
    path: `/dashboard/:siteId`,
    prepared: (params) => {...},
    routes: [
      { 
        path: `/dashboard/:siteId/widgets/:widgetId`,
        prepared: (params) => {...}
      },
      {
        path: `/dashboard/:siteId/friend/:friendId`,
        prepared: (params) => {...}
      }
    ]
  }
]

The type of `params` should automatically be known as `{siteId: string}` in the first route, `{siteId: string, widgetId: string}` in the second route, and `{siteId: string, friendId: string}` in the third route.

The type declaration above for `Routes` correctly constrains the path and prepare fields for a single object but does not handle the recursive nature of the configuration object, where each nested route can have a different generic since each path is unique. I am curious about whether this is achievable in TypeScript.

Here is a playground with the above code included

Answer №1

An easy solution here is to implement a builder function and create a data structure similar to a linked list. For example, consider the following:

interface Route<Path extends string> {
  path: Path;
  prepare(params: PathArgs<Path>): void;
}

type PathParams<
  Path extends string
  > = Path extends `:${infer Param}/${infer Rest}`
  ? Param | PathParams<Rest>
  : Path extends `:${infer Param}`
  ? Param
  : Path extends `${infer _Prefix}:${infer Rest}`
  ? PathParams<`:${Rest}`>
  : never;

type PathArgs<Path extends string> = { [K in PathParams<Path>]: string };

type ValidRoute = `/${string}/:${string}`

const route = <
  Path extends string,
  Routes extends Route<`${Path}${ValidRoute}`>[]
>(
  path: Path,
  prepare: (param: PathArgs<Path>) => void,
  ...routes: Routes
) => ({
  path,
  prepare,
  routes
})

const routes = <
  Str extends string,
  Elem extends Route<Str>[]
>(...elems: [...Elem]) =>
  elems

const result = [
  route(`/dashboard/:siteId`, (arg) => { },
    route(`/dashboard/:siteId/friend/:friendId`, (arg) => { }),
    route(`/dashboard/:siteId/widgets/:widgetId`, (arg) => { },)
  ),
  route(`/menu/:optioId`, (arg) => { },
    route(`/menu/:optioId/select/:selectId`, (arg) => { })
  )
]

Playground

The argument arg is properly inferred. There's no need for a complex recursive data structure. Try placing /menu/:optioId/select/:selectId into the dashboard namespace:

const result = [
  route(`/dashboard/:siteId`, (arg) => { },
    route(`/dashboard/:siteId/friend/:friendId`, (arg) => { }),
    route(`/dashboard/:siteId/widgets/:widgetId`, (arg) => { },),
    route(`/menu/:optioId/select/:selectId`, (arg) => { }) // error
  ),
]

You'll receive an error because the dashboard namespace expects strings with dashboard.

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

Angular 6 is experiencing a failure to send the Authorization Header

I have set up an HttpInterceptor to include an Authorization Header Token and I am attempting to handle http errors. However, the Authorization header is not being sent. Everything was working correctly before I added the error handler. I have also made su ...

Is the translation pipe in Angular 5 impure?

I'm currently utilizing ngx-translate. Can you tell me if the translation pipe is considered pure or impure? Also, would it be more beneficial to use the directive syntax translate="X" instead? ...

Verify if a particular string is present within an array

I am in possession of the key StudentMembers[1].active, and now I must verify if this particular key exists within the following array const array= ["StudentMembers.Active", "StudentMembers.InActive"] What is the method to eliminate the index [1] from Stu ...

Include a checkbox within a cell of a table while utilizing ngFor to iterate through a two-dimensional array

I am working with a two-dimensional array. var arr = [ { ID: 1, Name: "foo", Email: "<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="f5939a9ab5939a9adb969a98">[email protected]</a>", isChecked: "true" ...

The variable <variable> is not meeting the 'never' constraint. Error code: ts(2344)

Currently, I am attempting to utilize Supabase alongside TypeScript. However, I encounter an error when trying to use functions like insert(), update(), upsert(), etc. Specifically, the issue arises when declaring the object I want to declare: "Type & ...

DuplicateModelError: Unable to duplicate model after it has been compiled, React.js, MongoDB, TypeScript

In the early stages of developing an application using Next.js, Mongoose, and Typescript, I encountered a persistent issue. Whenever I attempt to send a request through Postman after clicking save, it fails, displaying the error message: OverwriteModelErr ...

Typescript integration with Sequelize CLI for efficient database migrations

According to the Sequelize documentation, it claims to work with Typescript. However, for it to be fully functional in a production environment, DB migration scripts are necessary. The issue arises when using the Sequelize CLI as it only generates and runs ...

Is it possible to combine JavaScript objects using TypeScript?

Currently, I am dealing with two objects: First Object - A { key1 : 'key1', key2 : 'key2' } Second Object - B { key1 : 'override a' } I am looking to combine them to create the following result: The Merged Re ...

Using TypeScript path aliases to resolve import errors

After creating a Vue.js project using Vue CLI 3 and setting up the import statement with the @ alias, I encountered an error when running npm run build. Can you explain why this happened? Error Message $ npm run build > [email protected] build / ...

Issue with the loss of scope in the Subscribe event causing the Clipboard Copy Event

Hey there, currently I am attempting to implement a text copying feature in Angular 2. I have a function that executes when a button is pressed, fetching data from the database and converting it into readable text, which is then automatically copied to the ...

Learn how to restrict input to only specific characters in an input box using Angular2 and validations

Is it possible to restrict user input in an input box to only specific characters such as '7' and '8'? I tried adding validations with attributes like type="number", min="7" and max="8", but even then other keys can be inserted before v ...

Stop access to specific pages or routes

I need help with restricting navigation in my Next.js app. While reading the documentation here, it mentions the importance of guarding against programmatically navigating to unwanted routes, but I'm unsure about how to implement this. Let's say ...

Async function causing Next JS router to not update page

I'm diving into the world of promises and JavaScript, but I've encountered an issue while working on a registration page following a tutorial on YouTube. Currently, I am using next.js with React and TypeScript to redirect users to the home page i ...

What is the method to retrieve Response Headers in cases of an empty response?

Currently, I am working with Angular2 and dotcms. My goal is to retrieve response headers after making a call to subscribe. const httpOptions = { headers: new HttpHeaders({ 'Content-Type': 'application/json' }) .append('Access ...

The output from Typescript Box results in a union type that is overly intricate to illustrate

Encountering an error while trying to render a Box: Received error message: Expression produces a union type that is too complex to represent.ts(2590) Upon investigation here, it seems that this issue arises from having both @mui/material and @react-thr ...

Exploring the use of Observables in Angular 2 services

Ensuring the seamless transfer of data between components is crucial in Angular development. One common way to achieve this is by utilizing observables. Let's take a look at how observables are implemented in a service: import { Injectable } from &ap ...

Triggering two function calls upon submission and then waiting for the useEffect hook to execute

Currently, I am facing a challenge with form validation that needs to be triggered on submit. The issue arises as some of the validation logic is located in a separate child component and is triggered through a useEffect dependency from the parent componen ...

The type 'TaskListProps[]' cannot be assigned to type 'TaskListProps'

I'm struggling with handling types in my TypeScript application, especially with the TaskListProps interface. export default interface TaskListProps { tasks: [ { list_id: string; title: string; description: string; status ...

Nestjs: Can't find property in Mongoose document

I am currently encountering some issues with the following code while using Nestjs along with Mongoose: import { Injectable } from '@nestjs/common'; import { Key, KeyDocument } from '@app/mongo'; import { Model } from 'mongoose&apo ...

The TypeScript error message is indicating that the variable is being used as a value, even though it is only defined as a type

I'm encountering a persistent TypeScript error that I can't seem to suppress, not even with @ts-ignore. Oddly enough, the code functions perfectly at runtime when TypeScript is disabled temporarily for testing. Is TypeScript misbehaving in this s ...