What is the process for ensuring that an object's property values must include specific substrings?

Exploring possibilities for an API configuration system and seeking guidance on typing a config object.

Clarification: This pertains to a resources/endpoints configuration for the API, where each resource/endpoint has a defined path containing specific parameters. I'll be using TypeScript terminology for simplicity and clarity.

The challenge lies in having a config object with properties that include nested properties requiring certain substrings.

const pathsSchema = {
  foo: {
    params: ["id"], // substring would be {id}
  },
} as const;

(I create a type from this object. The reason for choosing it to be an object instead of just a type is unclear; feel free to use a type directly.)

An example of the corresponding config object based on this schema:

const config = {
  foo: {
    path: "foo?id={id}",
  },
};

Dealing with a single substring is straightforward:

Path<param extends string> = `${string}{${param}}${string}`

type Paths<schema = typeof pathsSchema> = {
  [name in keyof schema]: {
    path: Path<schema[name]["param"]>
  };
};

However, handling multiple substrings requires generating all possible permutations, which is not efficient.
Update: Please refer to update at end of question.

I currently have a generic type that returns true if a string contains the necessary substrings:

type CheckPath<path extends string, params extends string[]> =
  // A. If there is a first param, get it.
  params extends [infer param extends string, ...infer rest extends string[]]
    ? path extends `${string}{${param}}${string}` // B. If path contains param...
      ? CheckPath<path, rest> // B. ...check next param...
      : false // B. ...else, path is invalid.
    : true; // A. There are no more params, so path is valid.

Below is the framework. The defineConfig helper aids in providing typing in a separate config file given by users (e.g., vite.config.ts for Vite).

const defineConfig = (config: Paths) => config;

const config = defineConfig({
  foo: {
    path: "foo?id={id}",
  },
});

Is there a way to ensure that the object passed to defineConfig passes the CheckPath validation? I'm open to alternatives to these types of "validation" types.

Update: Template literal types can now be intersected! Hooray for TypeScript! (Thanks to Alex Wayne for mentioning this.)
Now, my focus is on: how do I convert this schema with a map of names-to-string-arrays into a map of names-to-intersections-of-template-literal-types?

Answer №1

It's often better to opt for validation types that define the data you need instead of just returning true.


An interesting feature is the ability to intersect string template types, where both must be satisfied.

type AB = `${string}a${string}` & `${string}b${string}`
const str1: AB = `__a_b__` // works
const str2: AB = `b_a123` // works
const str3: AB = `a123` // error, missing `b`

This is a good starting point.


Next step is creating a utility type that converts a string[] of parameter names into an intersection of strings.

type UnionToIntersection<U> = 
  (U extends any ? (k: U)=>void : never) extends ((k: infer I)=>void) ? I : never

type Path<Params extends string[]> = UnionToIntersection<
    {
        [K in keyof Params & number]: `${string}{${Params[K]}}${string}`
    }[number]
>

type TestAB = Path<['a', 'b']>
//   ^? `${string}a${string}` & `${string}b${string}`

const pathGood: Path<['id']> = 'path?id={id}' // fine
const pathGood2: Path<['id', 'name']> = 'path?myname={name}&id={id}' // fine
const pathBad: Path<['id']> = 'asd?bad={bad}' // error

The Path type here takes a tuple of parameters and generates the respective string type for each param. We then index it by number to transform the object into a union of its values. Lastly, we use the amazing UnionToIntersection technique from this answer to merge them into the required intersection type.


That's one approach, but to cover the entire object, additional types are necessary.

type Paths<
    Config extends { [name: string]: { params: string[] } }
> = {
    [K in keyof Config]: {
        path: Path<Config[K]['params']>
    }
}

This type iterates over a config type, identifies the keys and corresponding parameter types needed for the final paths.

Using this, a schema type can be crafted:

type MyPathsSchema = {
  foo: { params: ["id"] },
  bar: { params: ["id", 'name'] },
}

Then, it can be applied in a function:

function defineConfig<
    MyPaths extends Paths<MyPathsSchema>
>(paths: MyPaths) {
  //...
}

Testing it out:

defineConfig({
    foo: { path: 'foo?id={id}' },
    bar: { path: 'foo?name={name}&id={id}' }
}) // works

defineConfig({
    foo: { path: 'foo?id={id}' },
    bar: { path: 'foo?name={name}' }
}) // Type '"foo?name={name}"' is not assignable to type
   //      '`${string}{id}${string}` & `${string}{name}${string}`'.

See 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

How can I export custom MUI theme definitions after overriding them?

I have successfully created a MUI theme for my project, and everything is functioning as expected. Now, I want to extract this theme as a separate library (e.g. @myproject/theme) so that I can easily share and redeploy it across multiple applications. This ...

What is the best way to pass the username and token data from a child component to its parent component

Hey, I'm currently working on a login app in React where the login component is a child of the app. My goal is to send back the username and token to the parent component once a user is logged in, so that I can then pass this information to other chil ...

Can the PrimeNG p-fileUpload component be configured to launch from a specific directory?

Utilizing the PrimeNG p-fileUpload component for file uploads. Looking to customize the default folder that opens when the select file button is clicked. Would like it to open in a specific location such as Desktop or Videos. Is there a method to achieve ...

Reconfigure an ancestral item into a designated key

I have a single object with an array of roles inside, and I need to transform the roles into an array of objects. See example below: Current Object: displayConfiguration: { widgetList: { widgetName: 'widget title', entityType: 'As ...

Converting JSON to Date in ES6/TS with Angular 2

Below is the JSON data: let JSON_RESPONSE = `{"birthDates": ["2017-01-02","2016-07-15"]}` There is a TypeScript object called Children with an array of Date objects and an ES6 constructor: class Children { birthDates: Date[] = [] constructor(values ...

Convert a fresh Date() to the format: E MMM dd yyyy HH:mm:ss 'GMT'z

The date and time on my website is currently being shown using "new date()". Currently, it appears as: Thu May 17 2018 18:52:26 GMT+0530 (India Standard Time) I would like it to be displayed as: Thu May 17 2018 18:43:42 GMTIST ...

Using ngFor to Filter Tables in Angular 5

Are you in the midst of implementing multiple data filters in an Angular Application that displays data using a Table and ngFor? You may have explored different methods such as using Pipe in Type Script, but discovered that it is not recommended accordin ...

Tips for incorporating an icon beneath the title of an angular echart

How can I add an icon below the Echart title? I have attempted the following but have been unsuccessful in rendering the icon. https://i.sstatic.net/PUURP.png The code in my component.ts file is as follows: constructor(private sanitizer: DomSanitizer) { ...

Is it possible to transfer files using web-bluetooth technology?

As I work on developing an embedded system that counts the number of cars, saves their speed and time data in a logs file using rsyslog. Simultaneously, I am creating a web-API (in Typescript/Angular with Electron for Desktop usage and later Web as well) t ...

Can declaration files be tailored for specific TypeScript versions?

My goal is to create an npm package for my type definitions to be used across various projects. I plan to name it @dfoverdx/ts-magic. Some projects are using TypeScript versions 3.x, while others are using >=4.2, which introduced new features like leadi ...

Uploading files in Angular 5 with additional properties of other objects

I am facing a challenge with uploading a file as part of a property to an object within a form. Most documentations I have come across only focus on services that handle standalone files. In my case, I have a form with various text inputs and date pickers, ...

React throwing an error when trying to use inline fontWeight styling with Typescript

I am currently working on applying a CSS rule to a td element. const boldText = { fontWeight: 'bold' } <td style={boldText}>Content</td> Unfortunately, I am encountering the following error: [ts] Type '{ style: { fontWeig ...

"Encountered a 'Module not found: Error: Can't resolve' issue while attempting to install from GitHub. However, the standard installation process

I am currently utilizing the Quilljs JavaScript library for a project in Angular. After installing it with the following command: npm install --save quill Everything appears to be functioning correctly, and I am able to import the Quill class into my Ty ...

express-typescript-react: The frontend bundle file could not be located (404 error)

Currently, I am in the process of developing a full stack application that utilizes Express (written in Typescript) and React. One key component of my development setup is webpack, which I'm using to bundle both the backend and frontend parts of the a ...

What is the best way to create and manage multiple collapsible Material-UI nested lists populated from an array with individual state in React

In my SideMenu, I want each list item to be able to expand and collapse independently to show nested items. However, I am facing the issue of all list items expanding and collapsing at the same time. Here is what I've attempted: const authNavigation ...

If you encounter the error message "The term 'Office' is not defined no-undef" it may be due to using a version of react-script that is newer than 3.0.0

I encountered an issue while creating an Outlook add-in using React and Typescript. When I tried to run my code with npm run start, I received the following error message, preventing me from running my React app. Failed to compile. ./src/index.tsx Line ...

What is the best way to combine TypeScript output while maintaining node import integrity?

Currently, I am combining the results of the typescript compiler using this particular technique. However, this process is causing issues with the imports of relative path modules in Node. The code below compiles and merges successfully; // Group.ts clas ...

Utilizing the dialogue feature within Angular 6

Situation: I am managing two sets of data in JSON format named customers and workers: customers: [ { "cusId": "01", "customerName": "Customer One", "email": "<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data- ...

Mastering the Correct Way to Import Flatbuffers using TypeScript

When working with typescript, I have been incorporating flatbuffers in the following manner: import {flatbuffers} from 'flatbuffers'; const builder = new flatbuffers.Builder(1); Subsequently, I convert to js for integration with react-native: ...

Failed to import due to an error from the downloaded dependency

I'm encountering an import error in the @react-three module of my downloaded package. ./node_modules/@react-three/drei/node_modules/troika-three-text/dist/troika-three-text.esm.js Attempted import error: 'webgl-sdf-generator' does not cont ...