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.