Tips on effectively setting limitations on TypeScript generics

In the process of developing this class, found at this playground link:

export class CascadeStrategies<
  T extends Record<any, (...args: any[]) => unknown>
> {
  private strategies: T = {} as T;

  constructor(strategyMap: T) {
    this.registerStrategies(strategyMap);
  }

  private registerStrategies(strategyMap: T) {
    this.strategies = strategyMap;
  }

  use(
    strategies: (keyof T)[],
    ...args: Parameters<T[keyof T]>
  ): ReturnType<T[keyof T]> {
    return this.strategies[strategies[0]](...args);
  }
}

The intended functionality of this class should be

const myMap = {
  test: (arg1: number, arg2: string) => arg1,
  otherTest: (arg1: number, arg2: string) => arg2,
  thirdTest: (arg1: number, arg2: string) => null
}
const cascadeStrats = new CascadeStrategies(myMap);
const shouldBeNumber = cascadeStrats.use(["test"], 0, "");
const shouldBeString = cascadeStrats.use(["otherTest"], 0, "");
const shouldBeNull = cascadeStrats.use(["thirdTest"], 0, "");

The goal is to have T represent an object where each entry is a function that can accept the same parameters and return a string. This is achieved by using

T extends Record<any, (...args: unknown[]) => string
. However, there is an issue with the typing, as
this.strategies[strategies[0]](...args)
results in type unknown, which does not match the expected ReturnType<T[keyof T]>.

If the type of strategies is changed from T to Record<any, any>, then

this.strategies[strategies[0]](...args)
will have the correct type and be inferred correctly when used. Although strategies is only an internal variable and does not impact user experience when using the class, it raises the question of what needs to be adjusted to achieve the desired outcome:

  • Accurate inference while utilizing the user-defined strategyMap (an object with functions that share the same parameters and return string).
  • strategies not having a type of Record<any, any>.
  • When the user employs cascadeStrats.use, they receive accurate inferences regarding the function's arguments and return type.

Answer №1

To simplify this, it is best to divide your generic type parameter into two separate components. Have A, representing the common parameter list type for all strategies, and T, denoting the mapping from strategy keys to their corresponding return types. With these distinctions, the type of strategies would be as follows:

type Strategies<A extends any[], T> =
  { [K in keyof T]: (...args: A) => T[K] }

This forms a mapped type that transforms each member of T into a function returning that member's value.

Here's how I would modify CascadeStrategies:

class CascadeStrategies<A extends any[], T extends object> {
  private strategies!: Strategies<A, T>

  constructor(strategyMap:
    Record<string, (...args: A) => any> &
    Strategies<A, T>
  ) {
    this.registerStrategies(strategyMap);
  }

  private registerStrategies(strategyMap: Strategies<A, T>) {
    this.strategies = strategyMap;
  }

  use<K extends keyof T>(
    strategies: K[],
    ...args: A
  ) {
    return this.strategies[strategies[0]](...args); // works fine
  }
}

This snippet compiles error-free. The crucial aspect here lies within the use() functionality. This function becomes generic based on K, a key in T. The inferred return type of use() becomes T[K], aligning with expectations.

For this setup to succeed, TypeScript needs to infer both A and

T</code when initializing <code>new CascadeStrategies(myMap)
. Inference can be intricate. My strategy was to define the constructor parameter type as
Record<string, (...args: A) => any> & Strategies<A, T>
. It utilizes an intersection, allowing each part to aid in inferring a distinct type argument. The
Record<string, (...args: A) => any>
type aids A's inference early on before TypeScript gathers details about T. Then, Strategies<A, T> helps in deducing T from the method return types. Simplifying the intersection down to just strategyMap being treated as type Strategies<A, T> enhances clarity.


Let's put it to the test:

const myMap = {
  test: (arg1: number, arg2: string) => arg1,
  otherTest: (arg1: number, arg2: string) => arg2,
  thirdTest: (arg1: number, arg2: string) => null
}
const cascadeStrats = new CascadeStrategies(myMap);
/*    ^? const cascadeStrats: CascadeStrategies<
    [arg1: number, arg2: string], 
    { test: number; otherTest: string; thirdTest: null; }
    >
*/
const shouldBeNumber = cascadeStrats.use(["test"], 0, "");
//    ^? const shouldBeNumber: number
const shouldBeString = cascadeStrats.use(["otherTest"], 0, "");
//    ^? const shouldBeString: string
const shouldBeNull = cascadeStrats.use(["thirdTest"], 0, "");
//    ^? const shouldBeNull: null
const shouldBeStringOrNumber = cascadeStrats.use(["test", "otherTest"], 0, "")
//    ^? const shouldBeStringOrNumber: string | number

cascadeStrats.use(["test"], "oops", "abc"); // triggers error!
// -----------------------> ~~~~~~
// Argument of type 'string' is not assignable to parameter of type 'number'.

The results appear satisfactory. T gets appropriately inferred, making shouldBeXXX match expected types, while A also receives accurate inferences, pinpointing any incorrect parameter types passed (as demonstrated in the last line).

Playground link to code

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

Enhancing the getDate function in JavaScript with additional days

My function for selecting the date is working perfectly: formatDateField(event: Date, formControl: string) { this.form .get(formControl) .patchValue( this.datePipe.transform(event.getTime(), "yyyy-MM-dd'T'HH:mm:ss&quo ...

Error in Angular compiler-cli: The namespace 'ts' does not contain the exported member 'ResolutionMode'

Currently working on a web application using Angular 16 in Webstorm. The application is still in the pre-release stage, with only minimal functionality completed so far. While editing with ng serve running to test changes as they were made, encountered an ...

What is the best way to encrypt the JWT decode token payload using Angular 8 on the client-side?

Currently, I am utilizing angular2-jwt to handle the code following an http.get request. In order to properly send an http post request, I must encode the body. let body = { "sub": "1234567890", "name": "John Doe", "iat": 1516239022 }; this.http.pos ...

NGXS no longer functions properly when upgrading to Angular 9 and enabling Ivy

I am currently in the process of upgrading my Angular application to version 9. I have encountered an issue where everything runs smoothly when Ivy is disabled, but enabling Ivy causes the application's serve task to not finish correctly: Here is a s ...

tips for accessing the output of a function within an object using TypeScript

I have developed a TypeScript module that simplifies the process. This function takes an object as input and executes all the functions while updating the values of the corresponding properties. If any property in the object is not a function, it will be ...

What are some effective strategies for incorporating React states with input variables?

As someone who is new to working with React, I am currently facing a challenge with my input form in React Typescript. My goal is to utilize the useState hook to store the values of various input fields such as name, email, and others. Currently, I have de ...

"Experiencing sluggish performance with VSCode while using TypeScript and Styled Components

My experience with vscode's type-checking is frustratingly slow, especially when I am using styled components. I have tried searching for a solution multiple times, but have only come across similar issues on GitHub. I attempted to read and understa ...

Encountering TS 2694 Error while running the ng serve command

I'm encountering some troublesome errors while attempting to run my angular application. Honestly, I can't figure out what's wrong, so I'm hoping someone here can assist me. I didn't make any significant changes, just added a singl ...

Is there a way to send both a file and JSON data in a single HTTP request?

Once I developed a small application using NestJs where I implemented a BFF (Backend for Frontend) service. Within this service, I tried to execute a POST request to create a new user while also including the user's avatar in the same request. Here is ...

"Defining the structure of object parameters in a function using type

What is the correct way to set typing for object style parameters? The function signature I have is as follows: private buildURI({ endpoint params }): void { } Typescript is throwing an error for missing typings, so I attempted the following: private ...

Using nodemailer to send an email with a dynamic variable that holds the HTML content

I am attempting to send a variable containing HTML code from a Vue component using the POST method. My technology stack includes TypeScript, Nuxt.js, Node.js, and Vue.js. const order_list = document.querySelector('table') as HTMLInputElement | n ...

Entering a series of predetermined value types into an array

I am currently trying to determine the best way to define a type for a specific value in TypeScript. The value in question looks like this: [{"source": "bar"}, 1483228800, 1484265600] Initially, I came up with the following approach: interface FieldSour ...

Change the country name to all lowercase letters

I'm attempting to retrieve the country flag icon of the Open Weather Map API. For example: The JSON response for the country code from the API request is in uppercase. To obtain the correct icon for the country, I need the country code in lowercase. ...

Error 404 Encountered During Azure Functions GET Request

I have double-checked the file and everything seems to be correct. The name matches the Azure website, the API Key is included, but I am not getting any response when trying to use this on Postman as a GET request. What could be the issue? //https://disn ...

Could the repeated utilization of BehaviorSubject within Angular services indicate a cause for concern?

While developing an Angular application, I've noticed a recurring pattern in my code structure: @Injectable(...) export class WidgetRegsitryService { private readonly _widgets: BehaviorSubject<Widget[]> = new BehaviorSubject([]); public get ...

Module augmentations do not allow for exports or export assignments

import { Request as ExpressRequest, Response as ExpressResponse } from 'express'; declare module 'kvl' { export = kvl; } declare const kvl: { ValidationDone:(param:(error: any, response: ExpressResponse) => void) => void; ...

Tips for adding text to a file in a VSCode Extension

Currently working on an exciting new VSCode extension project. Seeking advice on the best way to locate a file by name and insert text into it. Omitting any code here as it's not necessary at this point ;) Feeling a bit overwhelmed by the complexity ...

Transferring information from ag-Grid to a different user interface

I have implemented ag-Grid to showcase some data and now I want this data to be transferred to another interface upon clicking a cell in the grid. To achieve this, I utilized the cellRendererFramework by creating a custom component called RouterLinkRendere ...

Tips for fixing: "Object may be null" error in Angular routing

Currently, I am working on the angular heroes tutorial provided in the angular documentation and encountering an error. An issue has been detected, which states that the object is possibly 'null'. getHero(): void { const id = +this.route.snaps ...

Error: The specified property 'Body' is not found within the type '{}'

Looking for some assistance here. I've created an http.get method like this: return this.http .get(url) .map((response: Response) => { response = response.json(); // console.log('The http get response', respon ...