I have the following example code:
function call<T>(func: () => T): T {
return func();
}
const add = (a: number, b: number) => a + b;
call(add);
This of course fails with the following error:
Argument of type '(a: number, b: number) => number' is not assignable to parameter of type '() => number'.
Target signature provides too few arguments. Expected 2 or more, but got 0.
which is absolutely correct.
However, if we add another layer of indirection:
function call<T>(func: () => T): T {
return func();
}
function call_bad<T>(func: (...args: any[]) => T): T {
return call(func);
}
const add = (a: number, b: number) => a + b;
call_bad(add);
This compiles flawlessly and blows up on runtime with a
/b
set to undefined.
Why does this compile, and what can I do to fix it?
This is an even smaller MRE:
const add = (a: number, b: number) => a + b;
let f1: () => number;
let f2: ((...args: any[]) => number) = add;
f1 = f2;
Why does this compile?
Let's consider this simpler example:
const f: (...args: string[]) => string = a => a.toUpperCase()
f() // boom
This is clearly unsafe. The function implementation requires a string
parameter, but its type allows calling with an arbitrary number of string
arguments (including zero).
However this is not a bug, but an intentional design concession. TypeScript only checks that the source's parameter types are compatible with the target's rest parameter element types. The number of arguments required by the source is not part of the check.
The rationale behind this concession is to allow common JavaScript patterns to be written in TypeScript, such as calling String#replace
like so:
'blah blah blah'.replace(
/(.*) (.*) (.*)/,
(_, x, y, z) => `${x.toUpperCase()}-${y.repeat(2)}-${z.length}`
// ^^^^^^^ this would not be legal without the concession
)
The replacer
takes a rest parameter, yet we are allowed to satisfy its type with a function requiring a specific number of parameters.
You can read more in the discussions on issues like #9352 and #58552.