This code works, but I feel that it shouldn't. The higherOrder
function should complain that f
may only need one argument, but we're passing two.
type F1 = (a: number, b: number) => number;
type F2 = (a: number) => number;
type F = F1 | F2;
const f1: F1 = (a: number, b: number) => a + b;
const f2: F2 = (a: number) => 2 * a;
const higherOrder = (f: F): number => {
return f(2, 3);
}
const result1 = higherOrder(f1);
const result2 = higherOrder(f2);
console.log(result1, result2);
Why are there no compiler errors? Playground link: [link]
TypeScript takes the position that a function of fewer parameters is assignable to a function of more parameters, as described in the TypeScript FAQ and the TypeScript Handbook. As such, your F2
type is assignable to F1
, and therefore the union F1 | F2
is equivalent to just F1
. Indeed nothing changes if we use F1
instead of F1 | F2
, so we can dispense with talk of unions and just look at F1
:
const higherOrder = (f: F1): number => { return f(2, 3); }
const result1 = higherOrder(f1); // okay
const result2 = higherOrder(f2); // okay
It is considered type safe to call a function with more arguments than it expects, since it is very likely that such a function will simply ignore the excess arguments. As a convenience, the compiler will warn if it catches you actually making such a call directly:
const f1: F1 = (a: number, b: number) => a + b;
f1(2, 3); // okay
const f2: F2 = (a: number) => 2 * a;
f2(2, 3); // error
That error doesn't prevent any sort of runtime error, but it does possibly indicate a confusion on the part of the caller, so it is a warning. But TypeScript allows you to assign a function of type F2
to a value of type F1
, and therefore you can make such a call indirectly:
const fOne: F1 = f2; // okay
fOne(2, 3); // okay
This is also for convenience. Quite often when people pass a function as a callback, they don't give it as many parameters as the expected arguments it will be called with. Functional array methods are a common example:
[1, 2, 3].map(x => x + 1); // okay
The map()
array method will end up calling its callback with three arguments: the element, the index, and the whole array. If it were an error to make the assignment of F2
to F1
, then it would also be an error to make the assignment of x => x + 1
to the array callback, and you'd be forced to write something like
[1, 2, 3].map((x, i, arr) => x + 1);
which introduces i
and arr
for no discernible reason.
TypeScript doesn't intend to provide a perfectly consistent type system (see TS Non-Goal #3); instead it tries to issue warnings in situations which are more likely to be mistakes and suppresses them in situations which are more likely to be intentional. Calling a function directly with more arguments than necessary is more likely to be a mistake, so you get a warning. Assigning a function to a place which is likely to call it with more arguments than necessary is more likely to be intentional, so you don't get a warning.