Search code examples
typescriptoverloadingtype-inference

Why does TypeScript pick a different overload on function reference vs. a lambda-warpped call?


Why does TS pick the last overloaded variant, and not the middle f(x: string): string one in the last case, where it gives a type of (string | null)[] instead of string[]?

(TypeScript version 4.4.4)

function f(x: null): null;
function f(x: string): string;
function f(x: string | null): string | null;

function f(x: string | null): string | null {
  if (x === null) return x;
  return `${x}`;
}

f(null); // null
f('foo'); // string
f(Math.random() > 0.5 ? 'bar' : null); // string | null

['a', 'b', 'c'].map(x => f(x)); // string[]
['a', 'b', 'c'].map(f); // (string | null)[]

Solution

  • This is a design limitation in TypeScript. See microsoft/TypeScript#35501 for an authoritative answer.

    Overload resolution is only guaranteed to happen properly when you do not require any type inference to happen first. If you call the overloaded function directly with arguments of known types, that will work as expected. But any manipulation of the overloaded function signature where the compiler needs to infer types will use some heuristic to eagerly choose or synthesize a single call signature from the list of overloads without regard for which signature would actually be appropriate if called directly. This tends to be the last call signature (or maybe the first sometimes? according to this comment).

    In your case, the array map() method needs to infer a generic type parameter U corresponding to the return type of the callback function. The compiler eagerly picks the last call signature, sees that its return type is string | null, and goes with that:

    ['a', 'b', 'c'].map(f); // (string | null)[]
    

    This problem also occurs in conditional type inference including the popular Parameters<T> and ReturnType<T> utility types:

    type FParam = Parameters<typeof f>[0]; // type FParam = string | null
    type FReturn = ReturnType<typeof f>; // type FReturn = string | null
    

    For your example, if you want to call map() without wrapping f in x => f(x), you can sidestep the problem by manually specifying the generic type parameter:

    ['a', 'b', 'c'].map<string>(f); // string[]
    

    Now there's no type inference necessary... the compiler knows that U is string, and thus it needs to interpret f as a value of type (value: string) => string. Without inference, overload resolution happens as expected here, and so the second overload is chosen and no compiler error happens. And of course since you specified U as string, the return type of the map operation string[].

    Playground link to code