'How to create a function with a typed callback which ensures all callback parameters are passed to it

Given a function that accepts a callback with typed arguments, how can I make sure it doesn't allow functions without arguments.

I have added a sample code snippet that represents my problem

function x(cb: (a: number) => any) {
  cb(12)
}
x((a: number) => 1) // OK
x(() => 1) // OK
x((a: string) => 1) // Error: Argument of type '(x: string) => number' is not assignable to parameter of type '(a: number) => any'.

The second line should generate an error as this doesn't pass a complete list of parameters required by the callback

I want to enforce that every usage of function x should have all parameters in the callback as specified in the original function signature

Playground link



Solution 1:[1]

The general behavior of TypeScript is that it's okay to have a function which requires less arguments than what's provided. For example, when you have an onClick function (e: MouseEvent) => void it's fine to provide a function which does not use e.

So we have to get tricky here in order to disallow () => 1 as this function is assignable to (a: number) => any.

Solution

function x<T extends (a: number) => any>(cb: T & ([] extends Parameters<T> ? never : T)) {
  cb(12)
}
x((a: number) => 1) // OK
x(() => 1) // Error: Argument of type '() => number' is not assignable to parameter of type 'never'.
x((a: string) => 1) // Error: Argument of type '(x: string) => number' is not assignable to parameter of type '(a: number) => any'.

Explanation

I am using a generic type parameter T to represent the callback argument of our function x. We say that cb must be T, but we also impose an additional rule. We need to check the arguments of the callback which are Parameters<T>. If an empty array [] is assignable to these arguments ([] extends Parameters<T>), we say that cb must be T & never.

T & never is an impossible condition which causes us to get the red-underlined error on your second example. Here T is inferred as () => number so cb must be never. Therefore we get an error "Argument of type '() => number' is not assignable to parameter of type 'never'."


Your example function x doesn't return anything, but it could return cb(12) and get the correct return type based on the return type of the callback cb.

function x<T extends (a: number) => any>(cb: T & ([] extends Parameters<T> ? never : T)): ReturnType<T> {
  return cb(12)
}

TypeScript Playground Link

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Linda Paiste