Search code examples
typescriptoverloadingunion-types

No overload matches, while types are correct


I have an overloaded function f, with 2 possible arguments:

const f: {
  (x: number): void;
  (x: string): void;
} = (x: number | string) => {};

... as well as an array arr of values which should be passed to function f

const arr = [1, "1"];

... while type are CORRECT, typescript logs:

f(1);      // works
f("1");    // works

f(arr[0]); // error: No overload matches this call.

Why it shows this error, even so function f can accept both number and string from number | string possible values?


Solution

  • You've run into a longstanding missing feature of TypeScript; see the relevant feature request at microsoft/TypeScript#14107. When you call an overloaded function the compiler can currently only resolve the call to exactly one of the function's call signatures. It does not attempt to combine call signatures or resolve the call to multiple signatures. So even though conceptually a call to {(x: X): A; (y: Y): B;} with an argument of the union type type X | Y should be acceptable and return a value of type A | B, the compiler doesn't perform this sort of analysis.

    According to a comment by the TypeScript Program Manager, this isn't currently done because you can't just do this for all overloaded functions. For example, it would be a mistake to allow {(x: X, t: T): A; (y: Y, u: U): B} to be called like {(xy: X | Y, tu: T| U): A | B}, because you shouldn't be allowed to pass in an X for the first argument and a U for the second argument. So you'd have to detect the situation where you have multiple call signatures that differ in only one parameter type, and combine those. But according to the comment that's potentially inelegant, possibly expensive, and might have weird edge cases.

    That's the answer to the question as asked; if you want to see this happen you might want to give a 👍 to the feature request in GitHub, but it's unlikely to change much.


    You didn't ask about what to do instead, but until and unless this gets implemented, the recommended solution is to add another overloaded call signature that accepts what you want:

    const f: {
        (x: number): void;
        (x: string): void;
        (x: number | string): void; // <-- add this
    } = (x: number | string) => { };
    
    const arr = [1, "1"];
    
    f(1);      // works
    f("1");    // works
    f(arr[0]); // works
    

    although in the example above there is no reason to have the first two call signatures anymore since they both return void. If they returned different types then you might still want three call signatures.

    If your underlying use case doesn't allow you to redefine the function type, then you'll have to work around it in some way, such as by asserting that f accepts the argument you passed:

    (f as (x: number | string) => void)(arr[0]); // works
    

    Playground link to code