Confirm that two sets of strings are identical, providing detailed error information should they differ

I am currently developing type guards for an API framework and I aim to link the path parameters (a string) with a validation object.

My Desired Outcome:

The structure of my validation object is as follows:

const params = {
  firstArg: someValidationFunction,
  secondArg: someValidationFunction,
}

Here are the expected outcomes for the following strings:

  1. /api/some/path/{firstArg}/{secondArg}
    -> OK (both arguments are present in the string with correct names)
  2. /api/some/path/{secondArg}/{firstArg}
    -> OK (arguments' order does not affect validation)
  3. /api/some/path/{someOtherArg} -> Not OK (both arguments missing and unexpected argument found)
  4. /api/some/path/{firstArg}/{secondArg}/{someOtherArg}
    -> Not OK (unexpected argument found)
  5. /api/some/path/{firstArg}/secondArg
    -> Not OK (second argument missing as it lacks curly braces)

Progress So Far:

Referring to this blog post, I have implemented this type helper:

type ExtractPathParams<Path> = Path extends `${infer Segment}/${infer Rest}`
  ? ExtractParam<Segment, ExtractPathParams<Rest>>
  : ExtractParam<Path, {}>;

type ExtractParam<Path, NextPart> = Path extends `{${infer Param}}`
  ? Record<Param, any> & NextPart
  : NextPart;

This helper function retrieves the arguments from the path, and I have been attempting to compare the keys from that with the keys from the validation object. Inspired by this stackoverflow post:

function assert<T extends never>() {}
type Equal<A, B> = Exclude<A, B> | Exclude<B, A>;

type ValidationKeys = keyof params;
const one = '/api/some/path/{firstArg}/{secondArg}';
const two = '/api/some/path/{secondArg}/{firstArg}';
const three = '/api/some/path/{someOtherArg}';
const four = '/api/some/path/{firstArg}/{secondArg}/{someOtherArg}';
const five = '/api/some/path/{firstArg}/secondArg';

assert<Equal<keyof ExtractPathParams<typeof one>, ValidationKeys>>();
assert<Equal<keyof ExtractPathParams<typeof two>, ValidationKeys>>();
// @ts-expect-error
assert<Equal<keyof ExtractPathParams<typeof three>, ValidationKeys>>();
// @ts-expect-error
assert<Equal<keyof ExtractPathParams<typeof four>, ValidationKeys>>();
// @ts-expect-error
assert<Equal<keyof ExtractPathParams<typeof five>, ValidationKeys>>();

While this approach works, I would prefer to receive a type error indicating which keys are missing in the path based on the keys appearing in the ValidationKeys object instead of stating that string is not assignable to never.

Is such an outcome achievable?

Answer №1

One approach is to create a custom assertEqual() function that directly compares the two type arguments, rather than relying on Exclude, which discards information about the original types:

function assertEqual<T extends U, U extends V, V = T>() { }

This sets up a mutual constraint where T extends U and U extends T. While directly writing this in TypeScript would result in a circular definition, we can achieve similar behavior by establishing relationships between T, U, and V, with V having a default type argument of T. This setup technically avoids circularity because you have the flexibility to specify V as needed.

With this method, you can obtain the desired comparison results:

assertEqual<keyof ExtractPathParams<typeof one>, ValidationKeys>(); // valid
assertEqual<keyof ExtractPathParams<typeof two>, ValidationKeys>(); // valid
assertEqual<keyof ExtractPathParams<typeof three>, ValidationKeys>(); // error!
//          ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Type '"someOtherArg"' does not satisfy the constraint '"secondArg" | "firstArg"'.
assertEqual<keyof ExtractPathParams<typeof four>, ValidationKeys>(); // error!
//          ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Type '"secondArg" | "firstArg" | "someOtherArg"' does not satisfy 
// the constraint '"secondArg" | "firstArg"'.
assertEqual<keyof ExtractPathParams<typeof five>, ValidationKeys>(); // error!
//                                                ~~~~~~~~~~~~~~
// Type '"secondArg" | "firstArg"' does not satisfy the constraint '"firstArg"'.

For a hands-on demonstration, check out the code in the Playground link.

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

Innovative Functions of HTML5 LocalStorage for JavaScript and TypeScript Operations

Step-by-Step Guide: Determine if your browser supports the use of localStorage Check if localStorage has any stored items Find out how much space is available in your localStorage Get the maximum storage capacity of localStorage View the amount of space ...

Using TypeScript with React and Material-UI: Issue with undefined theme in createStyles()

Currently, I am delving into React with TypeScript and utilizing the Material UI framework for the frontend. In my quest to activate media queries, an error has crossed my path: Uncaught TypeError: Cannot read property 'up' of undefined ...

Can I modify a global array by updating a dynamically created array in the ngOnInit method of Angular?

Are there any suggestions on how to make a dynamic array available globally in Angular? I am currently using this codepen () which stores clicked countries in an array. The issue is that the array is nested within a function in ngOnInit and I need it to b ...

What is the best way to structure tabular data with metadata using TypeScript?

Our backend provides data in a specific format, with a data section containing tabular data and a meta section describing the columns in the table. The metadata includes information about the type of each column. For Example { meta: [ {name: "foo& ...

Using Fixed Patterns and Combining Types in an Interface

Presently, I am working with this interface: export interface User{ name: string birthday: number | Timestamp ... } When strictTemplates:false is enabled, I have no issue using this interface for server data retrieval with the birthday parameter in ...

Arranging an array of objects based on a specific keyword and its corresponding value

I have an array of objects that looks like this: [ { "type": "Exam", "value": 27 }, { "type": "Lesson", "value": 17 }, { "type": "Lesson", &qu ...

What could be causing the content in my select box to change only when additional select boxes are introduced?

When working with a form in next.js and using select boxes from material UI, I encountered an issue. The number of select boxes should change based on user input, but when I modify the value inside a select box, the displayed text does not update until I a ...

Implement FieldResolver in TypeGraphQL for an array of objects

My current dilemma revolves around a specific issue related to the definition of my Cart type, which is structured as follows: @ObjectType() export class Cart { @Field(() => ID) id: string; @Field((_type) => String) ownerId: String ...

Vue Basic Components 'T' has not been declared

After updating to Vue 3.4.30, I encountered an issue while trying to use Generic components. When attempting to use T as a type for a property, I received an error message. Any guidance or suggestions on how to resolve this would be greatly appreciated. I ...

The class 'GeoJSON' is mistakenly extending the base class 'FeatureGroup'

Encountering an error message in one of my projects: test/node_modules/@types/leaflet/index.d.ts(856,11): error TS2415: Class 'GeoJSON' incorrectly extends base class 'FeatureGroup'. Types of property 'setStyle' are incompa ...

Cannot locate AngularJS + Typescript controller

I'm encountering an error while attempting to integrate TypeScript with AngularJS. The issue I'm facing is: Error: [$controller:ctrlreg] The controller named 'MyController' has not been registered Does anyone have any insights on what ...

Pause for a few moments prior to initializing Bootstrap and Angular 2

I am currently developing a space/gravity game using Angular 2, and I am looking to incorporate a splash screen before the main menu component is loaded. I believe that the easiest approach would be to utilize the pre-bootstrapped contents of the index.ht ...

Having trouble accessing Vuex's getter property within a computed property

Can you help me troubleshoot an issue? When I call a getter inside a computed property, it is giving me the following error message: TS2339: Property 'dictionary' does not exist on type 'CreateComponentPublicInstance{}, {}, {}, {}, {}, Com ...

typescript event handling with oninput

When working with a slider, I am trying to detect when the user changes the value of the slider in order to display it. I have been following the tutorial at https://www.w3schools.com/howto/howto_js_rangeslider.asp. However, this code is not compatible wi ...

What steps do I need to take in order for TypeScript source files to appear in the node inspector?

Currently developing a node express app with TypeScript 2.3. Compiling using tsc Interested in debugging TypeScript code using node-inspector (now included in node 6.3+) SourceMaps are enabled in my tsConfig.json file { "compilerOptions": { "targ ...

Passing a generic type as a parameter in a generic class in TypeScript

TypeScript: I have a method in the DataProvider class called getTableData: public static getTableData<T extends DataObject>(type: { new(): T}): Array<T> { ... } Everything works fine when I use it like this: let speakers = DataProvider.getT ...

The type 'angular' does not have a property of this kind

Having trouble importing a method into my Angular component. An error keeps popping up: Property 'alerta' does not exist on type 'typeof PasswordResetService'. any I've double-checked the code and everything seems to be in order! ...

What is the method to assert that two arguments are equal within a function?

When working with TypeScript, you can pass a value to a function and have that function assert the value is true for type narrowing. For example: function assertTrue(v: unknown, message: string): asserts v { if (!v) { throw new MySpecialError(message ...

TSX is throwing an error: "No matching overload found for this call."

Just starting out with React and TypeScript here! I tried passing the propTypes into a styled-component and ran into this error message: Oh no, there's an issue with the overloads. Overload 1 of 2 seems to be missing some properties. Overload 2 of ...

Enhancing Code Completion Feature for Multiline Strings in Visual Studio Code

When attempting to include HTML code in a multiline string using backticks within TypeScript, I've noticed that VS Code doesn't offer auto-completion for the HTML tags. Take this example: @Component({ selector: 'app-property-binding&ap ...