typescriptunion-types

In Typescript, why is calling a union of functions not a type error?


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]


Solution

  • 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.

    Playground link to code