The concept of callback function overloading using generic types in TypeScript

Is there a way to define a callback type in TypeScript that can accept a variable number of generic type arguments while keeping the number of arguments fixed?

For instance:

export interface CustomFn {
  <T1>(value1: T1):  boolean
  <T1,T2>(value1: T1, value2: T2): boolean
  <T1,T2,T3>(value1: T1, value2: T2, value3: T3): boolean
}

This would allow code like:

const t: CustomFn = (a: string) => false
const t2: CustomFn = (a: number, b: string) => false
const t3: CustomFn = (a: boolean, b: string, c: number) => false

The above setup is close to what I need, but it's not functioning correctly.

The first line works as expected.

However, the second and third lines raise a compiler error:

Type '(a: number, b: string) => any' is not assignable to type 'CustomFn'

What is the correct way to declare a callback type that accepts a variable number of generic arguments with a fixed quantity?

Additional Information

This specific issue is part of a larger solution. Ideally, I want to achieve something like this:

export interface ValidationRule {
    name: string
    validator: CustomFn
}
const validationRules: CustomFn[] = [
   {
      name: 'required',
      validator: (s: string) => {
          return s != null
      }
   },
   {
      name: 'greaterThan',
      validator: (value: number, max: number) => {
          return value > max
      }
   }
]

Possible Solution

Maintainability is crucial. If necessary, types could be unioned like so:

export declare type GenericPredicate = 
    CustomFn<T1> |
    CustomFn<T1,T2> |
    CustomFn<T1,T2,T3> 


export interface ValidationRule {
    name: string
    validator: GenericPredicate
}
const validationRules: CustomFn[] = [
   {
      name: 'required',
      validator: (s: string) => {
          return s != null
      }
   },
   {
      name: 'greaterThan',
      validator: (value: number, max: number) => {
          return value > max
      }
   }
]

Answer №1

If you were seeking just a specificity, it would likely manifest as:

type UniqueFn = (...args: any[]) => boolean;

or potentially the more secure option:

type UniqueFn = (...args: never[]) => boolean;

which will validate any function that produces a boolean result.


However, there's a caveat: when you define a variable with this type, the compiler will unfortunately discard any specific information related to the number and types of parameters, only acknowledging that the function returns a boolean. The details are lost in translation.

The version with any[] will allow you to invoke the function with correct arguments:

type UniqueFn = (...args: any[]) => boolean;
const g: UniqueFn = (x: string) => x.toUpperCase() === x.toLowerCase();
g("okay");

Yet, it will also permit calling the function with incorrect parameters:

try {
    g(123); // unexpected behavior
} catch (err) {
    console.log(err) // RUNTIME ERROR 💥 x.toUppercase is not a function
}

On the flip side, the version with never[] is so strict that it won't even let you call the function with the right parameters:

type UniqueFn = (...args: never[]) => boolean;
const g: UniqueFn = (x: string) => x.toUpperCase() === x.toLowerCase();
g("okay"); // error, not permitted!

The issue here lies not so much within the UniqueFn type, but with the instances of that type.


My recommendation would be to swap out type annotations for the satisfies operator. When you declare const varName: Type = value, varName usually knows solely about Type and not potentially more precise typeof value. On the contrary, if you define

const varName = value satisfies Type
, the compiler ensures that value satisfies Type without broadening it. And varName's type remains typeof value, possibly more detailed than Type.

For the earlier sample code, this translates to:

const h = (
    (x: string) => x.toUpperCase() === x.toLowerCase()
) satisfies UniqueFn; // okay

h("okay"); // okay
h(123); // compilation error

Observe how h is verified as satisfying UniqueFn, yet the compiler retains its type as (x: string) => boolean, allowing h("okay") while flagging h(123).


For your provided code snippet, we can follow a similar approach:

interface ValidationCheck {
    name: string
    verifier: UniqueFn
}
const validationChecks = [
    {
        name: 'required',
        verifier: (x?: string) => {
            return x != null
        }
    },
    {
        name: 'greaterThan',
        verifier: (entry: number, maximum: number) => {
            return entry > maximum
        }
    },
    // Uncommenting below line reveals an error
    // { name: 'mistake', verifier: (a: string, b: number) => "uh-oh" }
] as const satisfies readonly ValidationCheck[];

Here, we utilize a const assertion to prompt the compiler to preserve the exact literal types of the name properties within the array elements. Instead of widening validationChecks to ValidationCheck[] and losing specificity, we simply use satisfies to ensure its compatibility.

If an error is made in defining validationChecks, it results in an error on satisfies:

const invalidChecks = [
    {
        name: 'required',
        verifier: (x?: string) => {
            return x != null
        }
    },
    {
        name: 'greaterThan',
        verifier: (entry: number, maximum: number) => {
            return entry > maximum
        }
    },
    { name: 'mistake', verifier: (a: string, b: number) => "uh-oh" }
] as const satisfies readonly ValidationCheck[]; // error!
// ----------------> ~~~~~~~~~~~~~~~~~~~~~~~~~~
// Type '{ name: "mistake"; verifier: (a: string, b: number) => string; }'
// is not compatible with type 'ValidationCheck'. 
// string cannot be assigned to boolean

Since validationChecks has not been expanded, the compiler can perform more accurate type checks:

const someCheck = validationChecks[Math.floor(Math.random() * validationChecks.length)];
/* const someCheck: {
    readonly name: "required";
    readonly verifier: (x?: string) => boolean;
} | {
    readonly name: "greaterThan";
    readonly verifier: (entry: number, maximum: number) => boolean;
} */

if (someCheck.name === "greaterThan") {
    someCheck.verifier(1, 2); // allowed
}

The compiler recognizes that someCheck is a discriminated union where the name property helps determine the type of verifier.

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

Organizing an array based on designated keywords or strings

Currently, I am in the process of organizing my logframe and need to arrange my array as follows: Impact Outcomes Output Activities Here is the initial configuration of my array: { id: 15, parentId: 18, type: OUTPUT, sequence: 1 }, { id: 16, parentId: ...

What is the best way to alter the Date format in Typescript?

Within my response, the field createdate: "2019-04-19T15:47:48.000+0000" is of type Date. I am looking to display it in my grid with a different format such as createdate: "19/04/2019, 18:47:48" while maintaining its original data type. To achieve this, I ...

Encountered an error while attempting to compare 'true' within the ngDoCheck() function in Angular2

Getting Started Greetings! I am a novice in the world of Angular2, Typescript, and StackOverflow.com. I am facing an issue that I hope you can assist me with. I have successfully created a collapse animation for a button using ngOnChanges() when the butto ...

Collaborating on code between a Typescript React application and a Typescript Express application

Struggling to find a smart way to share code between two interconnected projects? Look no further! I've got a React web app encompassing client code and an Express app serving as both the API and the React web app host. Since I use Typescript in both ...

Tips for identifying unnecessary async statements in TypeScript code?

We have been encountering challenges in our app due to developers using async unnecessarily, particularly when the code is actually synchronous. This has led to issues like code running out of order and boolean statements returning unexpected results, espe ...

What is the process for displaying all items within an object in Ionic 3?

Perhaps my question is not very clear, but what I am attempting to do is when the Feed item in the categories screen is clicked, all companies related to Feeding will be listed on the companies screen. I am quite confused because each category may have mu ...

Condition not applying in the Modal

I implemented *ngif on a button to show/hide it based on a condition, but it's not working as expected. The button should appear when an item is selected from ng-select. Here is the button code: <button *ngIf="switch" (click)="productSaveInCart() ...

Upon the initial render, the fetch request in the NextJS Client component is not triggered

When I load my home page, I want to display a collection of cards from a client component. However, the API fetch request in the client component does not trigger on the initial render, preventing the cards from appearing. To fix this issue, I have to manu ...

Designing a visual showcase with interactive tab links for image selection

I have been working on developing an Angular component that simulates a tab gallery functionality, inspired by this example. Below is my HTML structure: <div class="gallery-container"> <div class="display-container"> ...

Invoke a function and assign it to an export variable

I have a query regarding a file containing an export constant that is utilized to construct a navigation bar in CoreUI. However, I am exploring methods to generate dynamic JSON data within other Components or the same file and then inject it into the exp ...

Encountered a React select error following an upgrade: specifically, a TypeError stating that dispatcher.useInsertionEffect is not

Recently, I updated the react-select library and to my surprise, it stopped working altogether. Despite consulting the official site and the provided Upgrade guide, I couldn't find any helpful information. I also explored the samples on their website ...

Having a problem where the Next.js project is functioning in development mode, but encountering a "module not found" error

After following multiple tutorials to integrate Typescript into my existing app, I finally got it running smoothly in dev mode using cross-env NODE_ENV=development ts-node-script ./server/index.js However, when I execute next build, it completes successfu ...

Can the ElasticSearch standard Node client be considered secure for integration with cloud functions?

When working with my Typescript cloud functions on GCP, I have been making direct HTTP requests to an ElasticSearch node. However, as my project expands, I am considering switching to the official '@elastic/elasticsearch' package for added conven ...

When using Framer Motion for page transitions alongside React Router DOM v6, the layout components, particularly the Sidebar, experience rerenders when changing pages

After implementing page transitions in my React app using Framer Motion and React-Router-DOM, I noticed that all layout components such as the sidebar and navbar were unexpectedly rerendering upon page change. Here's a snippet of my router and layout ...

Validating React components with TypeScript using an array structure where the field name serves as the key

Trying to implement form validation with React. I have a main Controller that contains the model and manages the validation process. The model is passed down to child controllers along with the validation errors. I am looking for a way to create an array ...

Prisma Hack: excluding properties in type generation

EDIT hiding fields in the TypeScript definitions may pose a hidden danger: inaccessible fields during development with intellisense, but accidentally sending the full object with "hidden" fields in a response could potentially expose sensitive data. While ...

Utilize Page.evaluate() to send multiple arguments

I am facing an issue with the Playwright-TS code below. I need to pass the email id dynamically to the tester method and have it inside page.evaluate, but using email:emailId in the code throws an undefined error. apiData = { name: faker.name.firstNa ...

Different categories combined into a singular category

Trying to define a type that can be one of two options, currently attempting the following: type TestConfig = { file: string; name: string; } type CakeConfig = { run: string; } type MixConfig = { test: TestConfig | CakeConfig }; const typeCheck: M ...

Mapping a response object to a Type interface with multiple Type Interfaces in Angular 7: A step-by-step guide

Here is the interface structure I am working with: export interface ObjLookup { owner?: IObjOwner; contacts?: IOwnerContacts[]; location?: IOwnerLocation; } This includes the following interfaces as well: export interface IObjOwner { las ...

"Observables in RxJs: Climbing the Stairs of

Previously, I utilized Promise with async/await syntax in my Typescript code like this: const fooData = await AsyncFooData(); const barData = await AsyncBarData(); ... perform actions using fooData and barData However, when using RxJs Observable<T> ...