Home > Software engineering >  define function type with optional param in Typescript
define function type with optional param in Typescript

Time:05-02

How can define function type with which include function with or without parameters?

let param = 'test';

function main(callback: `some type`): void {
  if (need param) {
    callback(param);
  } else {
    callback();
  }
}

function func1():void {
 //
}


function func(param: string):void {
 //
}

main(func1);
main(func2, param);

With respect

CodePudding user response:

Typescript provides a utility function to extract function type arguments into a tuple. With this, it won't be a problem if the callbacks have optional parameters or no parameters at all.

let param = 'test';

function main<T extends (...args: any[]) =>any >(callback: T, ...params: Parameters<T>): void {
  callback(...params);
}

function func1():void {
 
}


function func(param: string):void {
 //
}

main(func1);
main(func, param);

CodePudding user response:

First of all, it seems like you want callback to be of the union type (() => void) | ((x: string) => void). That is, it is either a function that needs no parameters, or a function that needs one string-valued parameter.


In order for your (need param) pseudocode to work, we'd need a runtime check for the list of parameters a function expects. The only thing close to this I know of is the Function.length property which is the number of parameters the function requires:

function func0(): void {
    console.log("no param needed")
}
console.log(func0.length) // 0

function func1(param: string): void {
    console.log("I got my param and it was "   param.toUpperCase())
}
console.log(func1.length) // 1

Note that rest parameters don't add to the length:

function funcRest(...args: string[]) { }
console.log(funcRest.length) // 0

Nor do any optional parameters with a default value:

function funcOpt(x = "hey") { }
console.log(funcOpt.length) // 0

But this is complicated by the fact that not all JS runtimes accept rest or default parameters, so if you target an older runtime like ES5, then some of these will be downleveled to a function of a different length:

// --target=es5
function funcOpt(x = "hey") { }
console.log(funcOpt.length) // 1 instead of 0

This makes Function.length hard to model consistently in TypeScript, and so TypeScript only gives it a value of type number and not a numeric literal type. See microsoft/TypeScript#18422 for more information.

And that means you can't just check callback.length === 0 or callback.length === 1 inside the function and have the compiler understand what you're doing. If you'd like that check to narrow the type of callback to just ()=>void or just (x: string)=>void then you'll have to write a user-defined type guard function to tell the compiler that's what it does.

Like this:

function needsNoParam(x: Function): x is () => void {
    return x.length === 0;
}

function main(callback: ((x: string) => void) | (() => void)): void {
    if (!needsNoParam(callback)) {
        callback(param); // okay
    } else {
        callback(); // okay
    }
}

That compiles with no errors, and it behaves as expected:

main(func0); // no param needed
main(func1); // I got my param and it was TEST

So that works, but you're sort of fighting against TypeScript to get it done.


Still, there is an easier approach that works with TypeScript instead of fighting against it. TypeScript will allow a function with fewer parameters to be assigned to a variable which expects more parameters (of the same types). See this FAQ entry for more information.

What I mean is, even though you can't just call func0("oopsie") directly:

func0("oops") // error, expected 0 arguments but got 1

The compiler will let you assign func0 to a variable of type (x: string) => void:

const func1a: (x: string) => void = func0; // okay

And therefore call it with that extra parameter (through the new variable, at least):

func1a("okay") // okay

TypeScript takes the position that a well-written function will just ignore extra parameters passed to it, and so it is safe to let this happen, especially for callbacks. That might seem odd, but the alternative would be very annoying:

For example, when you write [1, 2, 3].map(x => x 1) you don't want the compiler to yell at you that x => x 1 doesn't match the expected callbackFn parameter to the Array.prototype.map() method; callbackFn will be called with three arguments: the array element, the array index, and the array itself. The callback x => x 1 only cares about the array element. Imagine if you had to write [1, 2, 3].map((x, i, a) => x 1) with completely unused i and a parameters in order to appease the compiler.

So that means you really don't need to spend any effort worrying about the difference between (x: string)=>void and ()=>void. Just treat them both as (x: string)=>void and pass in param all the time:

function main(callback: (x: string) => void) {
    callback(param);
}      

main(func0); // okay, no param needed
main(func1); // okay I got my param and it was TEST

That's a lot easier.

Playground link to code

  • Related