Overloaded method of an interface cannot accept a union type: "No overload matches this call"

Viewed 662

I use a 3rd party library that defines an interface (A) with an overloaded method (method). The method can accept either null or string value as a parameter.

I define a union type: string | null and pass it as a parameter into the method, but I get an error:

No overload matches this call.
  Overload 1 of 2, '(val: null): null', gave the following error.
    Argument of type 'StringOrNull' is not assignable to parameter of type 'null'.
      Type 'string' is not assignable to type 'null'.
  Overload 2 of 2, '(val: string): string', gave the following error.
    Argument of type 'StringOrNull' is not assignable to parameter of type 'string'.
      Type 'null' is not assignable to type 'string'.(2769)

Here is my code: TypeScript Playgroud

interface A{ 
  method(val: null): null;
  method(val: string): string;
}
type StringOrNull = string | null;

function myFunc(obj: A, val: StringOrNull) {
  obj.method(val); // Error: No overload matches this call.
}

Why does this happen and how I can fix this?

1 Answers

Well, that's because none of the overload signatures you provided accept string | null as the val parameter type. Consider this from the compiler's standpoint, in your A interface, you guaranteed that:

  1. method can accept null and return null
  2. method can accept string and return string

You then passed in a union of string | null - the compiler, as expected, complained that there is no (val: string | null) => unknown (or similar) signature ("no overload matches this call").

The error message tells you exactly what the compiler tried to do:

  1. assign string | null to string when checking (val: string) => string signature
  2. assign string | null to null when checking (val: null) => null signature

As you can clearly see, both types of val are incompatible with the StringOrNull union. I think in this case (and in general, frankly) it is better to write a constrained generic than function overloads (besides, your method is an identity function):

interface A{ 
  method<T extends StringOrNull>(val: T) : T;
}

type StringOrNull = string | null;

function myFunc(obj: A, val: StringOrNull) {
  obj.method(val); //OK
}

Playground


An aside: the handbook 2.0 specifically addresses this pitfall in an article about overloads:

TypeScript can only resolve a function call to a single overload

A second note stemming from the discussion in comments: if you do not control the declaration of the A interface, you can utilize the declaration merging technique to provide your own signature:

interface A{ 
  method(val: string) : string;
}

interface A {
  method<T extends StringOrNull>(val: T) : T;
}

type StringOrNull = string | null;

function myFunc(obj: A, val: StringOrNull) {
  obj.method(val); //OK
}

In case the interface is imported from a module, you will have to use the above paired with module augmentation.

Related