Handling union with an overloaded function that doesn't handle the whole union within a single overload

165 views Asked by At

I created an overloaded function with one argument and tried to call pass in an union that is "fully covered" by the various overloads - like this:

function overloaded(id: number): number;
function overloaded(ids: number[]): boolean;
function overloaded(idOrIds: number | number[]): number | boolean {
  if (Array.isArray(idOrIds)) return 0;
  return false;
}

function caller(idOrIds: number | number[]) {
  return overloaded(idOrIds);
}

But I get the following error when calling overloaded:

TS2769: No overload matches this call.

Overload 1 of 2, '(id: number): void', gave the following error.     Argument of type 'number | number[]' is not assignable to parameter of type 'number'.       Type 'number[]' is not assignable to type 'number'.
Overload 2 of 2, '(ids: number[]): void', gave the following error.     Argument of type 'number | number[]' is not assignable to parameter of type 'number[]'.       Type 'number' is not assignable to type 'number[]'.

It looks like TS wants a single overload to cover the whole union - how can I make my overloads work?

1

There are 1 answers

0
jcalz On BEST ANSWER

TypeScript does not automatically synthesize multiple call signatures into single signatures that take a union of arguments from different call signatures. There is an open issue suggesting this: microsoft/TypeScript#14107, but so far there hasn't been much happening with it. Someone would have to propose an implementation of this which would not degrade compiler performance or break real-world code.

You can provide the desired union call signature(s) yourself, like this:

function overloaded(id: number): number;
function overloaded(ids: number[]): boolean;
function overloaded(idOrIds: number | number[]): number | boolean; // add this
function overloaded(idOrIds: number | number[]): number | boolean {
    if (Array.isArray(idOrIds)) return 0;
    return false;
}

function caller(idOrIds: number | number[]) {
    return overloaded(idOrIds); // okay
}

Or, you could merge the multiple call signatures into a single generic conditional call signature:

function genericConditional<T extends number | number[]>(
  idOrIds: T
): T extends number ? number : boolean;
function genericConditional(idOrIds: number | number[]): number | boolean {
    if (Array.isArray(idOrIds)) return 0;
    return false;
}

const n = genericConditional(123); // number
const b = genericConditional([123]); // boolean
const nb = genericConditional(Math.random() < 0.5 ? 123 : [123]); // number | boolean

Note that unfortunately the generic conditional still needs to separate the implementation from the call signature (either via overload or via type assertion) because the compiler does not understand how to verify that a return value is assignable to a generic conditional type. There's an open issue for that too, microsoft/TypeScript#33912.


If you only have a few overloads then the extra overload is fine. But as the number of overloads increases, adding call signatures to handle every subset of them becomes untenable. In those cases you'll find that a single generic conditional call signature will scale much better.


Playground link to code