Home > database >  What is the best way to define a type of dynamic callback?
What is the best way to define a type of dynamic callback?

Time:03-21

I have (in TypeScript) a class called 'Instructions', which has a static method called 'operate'. This method accepts two certain arguments, and two more possible ones. The first argument is a 'callback function' that the 'operate' method should call, and the rest of the arguments are the same arguments that will be passed to the callback function I got as the first argument. My question is, how to define the type of the callback function.

At first, I thought of implementing it like this:

interface Operand {
  // ...
}

interface OperationFunc {
  (dstOperand: Operand, srcOperand?: Operand, powerEvaluation?: number): void;
}

class Instructions {
  static operate (callback: OperationFunc, dstOperand, srcOperand?, powerEvaluation?) {
    callback(dstOperand, srcOperand, powerEvaluation);
  }
}

Which turned out to be unsuccessful for several reasons. The main reason is that the callback function is not the same function in every call. But - these are different functions, and each time I pass a different callback function as needed. Sometimes it is a function that receives a single 'Operand' type argument, sometimes it is a function that receives two 'Operand' type arguments, and sometimes it is a function that receives three arguments - Two of them are of the 'Operand' type, and the third of the 'number' type. These three type of functions return void.

interface Operand {
  // ...
}
type Function1 = (dst: Operand) => void; // The first type of callback functions
type Function2 = (dst: Operand, src: Operand) => void; // The second type of callback functions
type Function3 = (dst: Operand, src: Operand, powerEvaluation: number) => void; // The third type of callback functions

Does anyone have any idea how to define a Type of such a callback function?

CodePudding user response:

Overload signatures will get you the right idea.

static operate (callback: (a: Operand) => void, dstOperand: Operand);
static operate (callback: (a: Operand, b: Operand) => void, dstOperand: Operand, srcOperand: Operand);
static operate (callback: (a: Operand, b: Operand, c: number) => void, dstOperand: Operand, srcOperand: Operand, powerEvaluation: number);
static operate (callback: (a: Operand, b: Operand, c: number) => void, dstOperand?: Operand, srcOperand?: Operand, powerEvaluation?: number) {
  callback(dstOperand, srcOperand, powerEvaluation);
}

Now there's one function which can be called in four ways. So it will accept all of these calls.

Instructions.operate((a) => {}, 0);
Instructions.operate((a, b) => {}, 0, 0);
Instructions.operate((a, b, c) => {}, 0, 0, 0);

But it will reject this one

Instructions.operate((a, b) => {}, 0);

One word of warning. This signature will also accept the following (probably mistaken) call.

Instructions.operate((a) => {}, 0, 0);

And that's because the type of (a) => {} is inferred as (a: Operand) => void. A function of type (a: Operand) => void is perfectly allowed to take two arguments, or three arguments, or as many arguments as you want. Javascript (and Typescript) always allows a function to take extra arguments and will silently discard them. Since Typescript can't tell the difference between a function of one argument and a function of two arguments which ignores its second argument, it thinks (a) => {} is actually a function of two arguments in this context. I don't know of a way to get around this, given that the type checker is actually correct that the code will run (although it's likely a programmer error).

CodePudding user response:

With generics and utility types, you can define a type that gets applied generically to the types that you provide as arguments. For instance, you can define a function that takes another function as a parameter, as well as the parameters of that function.

function wrapper<
  T extends (...params: any[]) => any
>(func: T, ...params: Parameters<T>): ReturnType<T> {
  return func(...params);
}

function theFunc(x: string, y: number, z: string) {
  return x   " "   y    " "   z;
}

console.log(wrapper(theFunc, "i would like", 1, "tall glass of milk"));

See it on the playground.

CodePudding user response:

We can make use of overloads to describe the possible behaviours of this function:

    static methymethod(callback: Function1, ...args: Parameters<Function1>): void;
    static methymethod(callback: Function2, ...args: Parameters<Function2>): void;
    static methymethod(callback: Function3, ...args: Parameters<Function3>): void;
    static methymethod(callback: Function1 | Function2 | Function3, ...args: Parameters<Function1 | Function2 | Function3>) {

The first 3 "declarations" declare each overload for each function, and the last one is simply all of them combined.

However when attempting to call the callback we get an error:

The 'this' context of type 'Function1 | Function2 | Function3' is not assignable to method's 'this' of type '(this: null, dst: Operand) => void'.
  Type 'Function2' is not assignable to type '(this: null, dst: Operand) => void'.(2684)

This is because TypeScript does not know that the function called should receive the correct arguments, but we know it is safe if the overload signatures are being enforced.

For runtime type checking, you could check the length of args, but this error can either be solved by ignoring it with @ts-ignore or changing the type of callback to a more generic one like Function or even any.

Playground

  • Related