microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
100.72k stars 12.45k forks source link

Support overload resolution with type union arguments #14107

Open johnendev opened 7 years ago

johnendev commented 7 years ago

TypeScript Version: 2.1.6

Code

interface Foo {
    bar(s: string): void;
    bar(n: number): number;
    bar(b: boolean): boolean;
}
type SN = string | number;

var sn1: string | number;
var sn2: SN;
var foo: Foo;

var x1 = foo.bar(sn1); // error
var x2 = foo.bar(sn2); // error

Expected behavior: This should be allowed. The type of x1 and x2 should be void | number, the union of the matching overload return types.

All 3 overloads can be seen as a single overload with a union type. It should try to fallback to a union when it can't match one of the originally defined overloads.

Actual behavior: error TS2345: Argument of type 'string | number' is not assignable to parameter of type 'boolean'. Type 'string' is not assignable to type 'boolean'.

DanielRosenwasser commented 7 years ago

I can't find other issues where this discussion's been had, but I'll try to give some context. The single-parameter case always tends to be the most frustrating one for people, and the most obvious one to fix. The problem occurs when you have multiple overloads.

For example, suppose you had the following overloads

interface Foo {
    bar(s1: string, s2: string): void;
    bar(n1: number, n2: number): number;
}

and you tried calling with the following:

declare var sn1: string | number;
declare var sn2: string | number;
declare var foo: Foo

foo(sn1, sn2);

Should our call to foo succeed? Probably not, since we can't guarantee that sn1 and sn2 share the same type.

So we could come up with a way of trying to collapse overloads that differ by exactly one parameter into unions and retying the overload process, but I think our concerns are

  1. It seems a little ad-hoc, and ideally we'd like to generalize the behavior.
  2. That process could be expensive (although this is less of a concern - this could be lazily done if overload resolution fails).
  3. This process is potentially not as trivial as it sounds at face value.
johnendev commented 7 years ago

Ok. I think a more general way to look at it would be this: When resolving overloads, an overload must match for every tuple in the cartesian product of all union argument types. Again the return type would be the union of all matching overloads.

For your example, it would look for overloads for each of (string, string), (string, number), (number, string), (number, number). It would fail as 2 of them don't have a matching overload.

The problem space would grow exponentially, but functions generally don't have a large number of parameters with a large number of types in each union. It could be short-circuited to ensure that every necessary type exists at each positional parameter first, before computing permutations. For generic parameters short-circuiting might not always be possible.

RyanCavanaugh commented 7 years ago

1805 was the prior incarnation of this

masaeedu commented 7 years ago

@DanielRosenwasser Say we treat functions of the form (a: A, b: B) => R identically to functions of the form (a: A) => (b: B) => R for the purposes of overload resolution. We only need a sensible specification for the return type of the overloaded function ((a1: A1) => R1) & ((a2: A2) => R2) when invoked with A1 | A2, and we can then apply this recursively to the return type until we've exhausted the parameter list.

I propose that the return type of the overloaded function ((a1: A1) => R1) & ((a2: A2) => R2), when invoked with argument A1 | A2 be R1 | R2 (when invoked with A1, be R1, etc). Note that R1 | R2 is a union of the partially applied remainders from each overload, so this is a union of functions.

Now we need a sensible rule for the return type of a union of functions R1 | R2 == ((b1: B1) => S1) | ((b2: B2) => S2), and what it can be invoked with. I propose that the union ((b1: B1) => S1) | ((b2: B2) => S2), be invocable with an intersection of the parameter types B1 & B2, and that it return S1 | S2. Note that this is not a perfect dual of the previous rule; the uncertainty that was introduced due to invocation with a union propagates throughout the remaining parameters.

Let's apply this to your example and see what we come up with:

  1. (s1: string, s2: string) => void & (n1: number, n2: number) => number; will be substituted with (s1: string) => (s2: string) => void & (n1: number) => (n2: number) => number; for the purposes of overload resolution
  2. We apply the argument string | number to the function type (s1: string) => S & (n1: number) => N to obtain the return type S | N, for S == (s2: string) => void and N == (n2: number) => number
  3. We try to apply the argument string | number to the function type (s2: string) => void | (n2: number) => number, but find that we cannot, since it can only accept a parameter of type string & number. The program does not succeed type checking
  4. If we had provided string & number for the second argument, type checking would succeed and the overall return type would be void | number

The requirement for the second argument to be of type string & number intuitively makes sense, and simply falls out of the rules described above. I think this would generalize fairly well to any number of overloads and any number of parameters.

jcalz commented 7 years ago

It seems like this issue keeps being reported occasionally. Folks expect TypeScript to notice that an overloaded or generic function (essentially an intersection of functions) can take an argument of a union of possible argument types; and that a union of functions can take an intersection of possible argument types.

The former case is mentioned in this issue and in some of the references above.

The latter case shows up when you have something like this:

var someArray = Math.random() < 0.5 ? [1,2,3] : ['a','b','c']; // number[] | string[]
var filteredArray = someArray.filter(x:any => typeof x !== 'undefined') // nope!

(see #16716, #16644)


You can force TypeScript to notice this in specific cases:

function intersectFunction<A1, R1, A2, R2>
  (f: ((a: A1) => R1) & ((a: A2) => R2)) :    
  ((a: A1 | A2) => (R1 | R2)) {
  return f;
}

function uniteFunction<A1, R1, A2, R2>
  (f: ((a: A1) => R1) | ((a: A2) => R2)) :    
  ((a: A1 & A2) => (R1 | R2)) {
  return f;
}

which is, I think, the translation of part of @masaeedu's method into explicit functions.

Then @johnendev's case could be forcibly fixed like this:

// behold ugliness:
var boundBar = foo.bar.bind(foo) as typeof foo.bar; // have to bind to call later
var x1 = intersectFunction<string, void, number, number>(boundBar)(sn1); // number | void
var x2 = intersectFunction<string, void, number, number>(boundBar)(sn2); // number | void
// correct, but at what cost?

and the case with Array.filter() would be similarly mangled into type checking like this:

// behold ugliness:
var boundFilter = someArray.filter.bind(someArray) as typeof someArray.filter; // ditto
var filteredArray = uniteFunction
  <(n: number) => any, number[], (x: string) => any, string[]>
  (boundFilter)((x: any) => typeof x !== 'undefined'); // number[] | string[]
// correct, but at what cost?

But it would be much nicer all around if TypeScript could infer this itself. @masaeedu's idea about using currying/uncurrying to do this inference of polyadic functions is pretty neat. Does anyone think this would be incorrect as opposed to just possibly too expensive for the type checker?

Thanks!

aj-r commented 7 years ago

I think that this union type argument check should only be done if all of the following apply:

  1. An exact match on any one overload was not found. This not only saves a bunch on compiler performance; it also lowers the risk of breaking backwards compatibility.
  2. Two or more overloads have almost the same signature, with exactly one argument being different. This cuts down on the number of combinations we need to check for. I'm not sure if it will cover all use cases - just an idea at this point.
robyoder commented 6 years ago

Ran into this myself recently. I was pretty surprised to find how old this issue is, but it sounds like it may be more complicated than it appears to a user like me. :/

saschanaz commented 6 years ago

This is needed to type FormData.append correctly.

interface FormData {
    append(name: string, value: string): void;
    append(name: string, blobValue: Blob, filename?: string): void;
}

Currently we want to allow string | Blob union so we type it as append(name: string, value: string | Blob, filename?: string): void. This incorrectly allows append("str", "str", "str"); that throws on Firefox.

See https://github.com/Microsoft/TSJS-lib-generator/pull/432#discussion_r181137305

aj-r commented 6 years ago

I think this is no longer needed now that we have conditional types.

@saschanaz for you case, you could do this:

interface FormData {
    append(name: string, value: string | Blob): void;
    append(name: string, blobValue: Blob, filename: string): void;
}
aoberoi commented 6 years ago

I think this is no longer needed now that we have conditional types.

@aj-r do you mean that there is some generalizable solution to this problem since conditional types have been introduced? if so, can you provide an example?

here's a somewhat simplified version of this issue in a real library that i'm facing (see @types/got@8)

interface GotFormOptions<E extends string | null> {
    body?: {[key: string]: any};
    form: true;
    encoding?: E;
}

interface GotBodyOptions<E extends string | null> {
    body?: string;
    encoding?: E;
}

interface GotFn {
    (url: string, options: GotFormOptions<string>): Promise<string>;
    (url: string, options: GotBodyOptions<string>): Promise<string>;
    (url: string, options: GotBodyOptions<null>): Promise<void>;
}

declare const got: GotFn;
declare const options: GotFormOptions<string> | GotBodyOptions<string>;

const hi = got('hello', options) // error Argument of type 'GotFormOptions<string> | GotBodyOptions<string>' is not assignable to parameter of type 'GotBodyOptions<null>'
aj-r commented 6 years ago

@aoberoi yes, I should have explained more.

What I meant is you should be able to use conditional types to simplify multiple overloads into a single overload. For example, the case in the original feature request:

interface Foo {
    bar(s: string): void;
    bar(n: number): number;
    bar(b: boolean): boolean;
}

Using conditional types, this can now be simplified into a single overload:

interface Foo {
    bar<T extends string | number | boolean>(value: T):
        T extends string ? void : 
        T extends number ? number :
        boolean;
}

declare const foo: Foo;
foo.bar("baz"); // void
foo.bar(2);  // number
foo.bar(true); // bolean

declare const value: string | number | boolean;
foo.bar(value); // void | number | bolean

Although in this particular case, you might consider changing void to undefined, since void | number | boolean seems like a strange type.

I believe this approach should work for all use cases, but I'm not 100% confident of that.

In your case, I believe this is a mistake in @types/got. The first 2 overloads can (and should) be combined into a single overload. This is simple enough that conditional types are not required:

interface GotFn {
    (url: string, options: GotFormOptions<string> | GotBodyOptions<string>): Promise<string>;
    (url: string, options: GotBodyOptions<null>): Promise<void>;
}

Submit a pull request to DefinitelyTyped if you want to fix this.

felipeochoa commented 6 years ago

I think you're right that overloads can be re-written as generics + conditionals, but it is very clunky. Even your simple Foo example is hard to quickly parse, and it doesn't even have generics parameters or types of its own. And you have to introduce tuples if you want to handle multiple parameter overloads:

interface Foo {
    bar(s1: string, n1: number): void;
    bar(n2: number, s2: string): number;
}

Would have to turn into something like:

type A1 = [string, number];
type A2 = [number, string];
type R<T extends A1 | A2> = T extends A1 ? void : number;
interface Foo {
    bar<T extends A1 | A2>(s1: A1[0], s2: A2[1]): R<T>;
}

which would be quite unwieldy to try to do inline.

The transformations seems fairly mechanic though, so perhaps this is something the compiler could do internally to resolve the spurious errors above

aj-r commented 6 years ago

@felipeochoa I think you meant this?

type A1 = [string, number];
type A2 = [number, string];
type R<T extends A1 | A2> = T extends A1 ? void : number;
interface Foo {
    bar<T extends A1 | A2>(s1: T[0], s2: T[1]): R<T>;
}

Either way, I don't think this is correct, because it would allow this:

declare const foo: Foo;
foo.bar("a", "b");

In this case I think you should NOT combine them into a single overload. Combining should only be done if all parameters but one are the same.

felipeochoa commented 6 years ago

@aj-r Thanks, that is what I meant. It seems like we therefore still need overloads and still have this issue

jcalz commented 6 years ago

Well, you can use @masaeedu's suggestion for how to compose/curry functions and get some interpretation for overloads involving more than one argument, but it gets hairy. Two overloads with two arguments each looks something like:

// convert overload of form {(a: T, b: U) => V  & (a: W, b: X) => Y} to a single function
type TwoArgOverloadToConditionalTypes<T, U, V, W, X, Y> = <A extends T | W>(
  a: A,
  b: ((A extends T ? (x: U) => void : never) | (A extends W ? (x: X) => void : never)) extends
    ((x: infer I) => void) ? I : never
) => (A extends T ? V : never) | (A extends W ? Y : never)

Let's see if we can do @felipeochoa's version of Foo:

interface Foo {
  bar: TwoArgOverloadToConditionalTypes<string, number, void, number, string, number>
}
declare const foo: Foo;
foo.bar("a", 1); // okay, void
foo.bar(1, "a"); // okay, number

foo.bar("a", "b"); // error, "b" is not assignable to number
foo.bar(1, 2); // error, 2 is not assignable to string

const numberOrString = Math.random() < 0.5 ? "a" : 1
foo.bar(numberOrString, "b"); // error, "b" is not a number 
foo.bar(numberOrString, 2); // error, 2 is not a string
// it wants the second argument to be number & string
// there is no such animal, but we'll pass something that matches: namely, never
foo.bar(numberOrString, null! as never); // number | void

Those behaviors look plausible to me. I don't know it changes anyone's opinion on the continued relevance of this issue, of course. Cheers!

LinusU commented 6 years ago

I think that I'm hitting this error as well, I have an interface that accepts one file, or a list of files, and I cannot accept both in my function and pass straight thru:

type InputFile = string | {foo: 1}

interface Test {
  add(files: InputFile[]): void
  add(file: InputFile): void
}

function a (test: Test, files: string | string[]) {
  test.add(files)
}

function b (test: Test, files: string | string[]) {
  if (typeof files === 'string') {
    test.add(files)
  } else {
    test.add(files)
  }
}

Function a errors, while function b works.

Ran into this while trying to use the Browserify API.

thw0rted commented 6 years ago

Maybe I'm not getting something that's in the existing comments, but is there a current status for this issue? Is it being worked? Can it be fixed? If so, is there any kind of timeline?

lsagetlethias commented 5 years ago

It also happens with literal types:

// String Literal
class C1 {
    static bar(p: 'foo'): boolean;
    static bar(p: string): any {}

    static baz(p: string): boolean;
    static baz(p: 'foo'): any {}
}

const a = C1.bar('test'); // error
const b = C1.baz('foo'); // not "any"

// Numeric Literal
class C2 {
    static bar(p: 1): boolean;
    static bar(p: number): any {}

    static baz(p: number): boolean;
    static baz(p: 1): any {}
}

const c = C2.bar(2); // error
const d = C2.baz(1); // not "any"

// Boolean Literal
class C3 {
    static bar(p: true): boolean;
    static bar(p: boolean): any {}

    static baz(p: boolean): boolean;
    static baz(p: true): any {}
}

const e = C3.bar(false); // error
const f = C3.baz(true); // not "any"

The @aj-r solution with conditional typing with generic is working only for the return type ; but the auto completion doesn't work...

Playground link

burtek commented 5 years ago

Just out of curiosity: what's the status of this issue? šŸ¤”

ExE-Boss commented 5 years ago

https://github.com/Microsoft/TypeScript/pull/30586 provides aĀ bandā€‘aid solution forĀ RegExp.

/cc @sandersn

borekb commented 5 years ago

For reference, ways to deal with this (pick your poison):

// Overloads in external-library.d.ts
function someFunction(a: string): string;
function someFunction(a: number): string;
// Wrapper that accepts union type
function wrapperOfSomeFunction(a: string | number) {

  // Err: Argument of type 'string | number' is not assignable to parameter of type 'number'
  const result0 = someFunction(a);

  // Option 1:
  let result1: string;
  if (typeof a === "string") {
    result1 = someFunction(a);
  } else {
    result1 = someFunction(a);
  }

  // Option 2:
  const result2 = typeof a === "string" ? someFunction(a) : someFunction(a);

  // Option 3:
  const result3 = someFunction(a as any);
}

I ended up using "as any".

ExE-Boss commented 5 years ago

This is even worse when the code isĀ JavaScript and Iā€™m using tscĀ --allowJsĀ --checkJsĀ --noEmit, because then the only way to cast a to any is to do:

const result3 = someFunction(/**@type {any}*/(a));

Which looks a lot more ugly.


TL;DR: result0 should be valid.

osdiab commented 5 years ago

I have also run into this issue, in the form of calling a function with a parameter decided via indexed-access notation on a union type.

My concrete situation is that I've specified all my API routes with io-ts codecs, which I'm sharing between a frontend and backend to extend type safety across the network boundary. I have a queryApi function that takes in the name of the route as a generic to index into the union that defines all these API routes, and then calls the proper functions to encode or decode data to and from the network; but it can't narrow properly because of this issue.

Luckily this is only done in one place, so casting to any isn't the worst thing in the world, but it would be nice if we didn't have to.

A simplified version of my scenario is the following:

const stuff = {
    foo: (a: string) => { console.log(a) },
    bar: (a: number) => { console.log(a) },
}

function pickSomething<K extends keyof typeof stuff>(
    key: K, arg: Parameters<typeof stuff[K]>[0]
): ReturnType<typeof stuff[K]> {
    return stuff[key](arg);  // angry! `arg` is too general
}

In my case, @borekb 's proposed workarounds don't work for my project except the any case, because while it's physically possible to explicitly check the types of every API route in my app, it would be monstrous since there are so many (Imagine stuff in my example has 30+ members with all different types) and their type signatures are not terse like string.

Though luckily, it is only one argument in my case, so if this were implemented at least for one argument, that would solve my issue.

CG65RqXP commented 5 years ago

I've also encountered this problem. Additionally, I've noticed that:

declare const x: {
  (a: true): void;
  (a: false): void;
};
declare const y: boolean;

y === true? x(y) : x(y);

always works, while y? x(y) : x(y) works only when structNullChecks is on.

fan-tom commented 4 years ago

I've also encountered this problem. Additionally, I've noticed that:

declare const x: {
  (a: true): void;
  (a: false): void;
};
declare const y: boolean;

y === true? x(y) : x(y);

always works, while y? x(y) : x(y) works only when structNullChecks is on.

because without strictNullChecks y may be null|undefined and x is not callable with any of them?

waterplea commented 4 years ago

Could we at least add union overloads to built-in JavaScript types? I'm working with Web Audio API and I already ran into this issue twice:

const DEFAULT_LENGTH = 10;

function makeFloat32Array(array?: number[]): Float32Array {
    return new Float32Array(array || DEFAULT_LENGTH);
}

https://www.typescriptlang.org/play/#code/MYewdgzgLgBAIgUQGIEECqAZAKgfQwgOQHEsAJGAXhgEYAGAbgChGAzAVzGCgEtwYBbAIYBrAKZIANiEFQAzACYUAJyWCAngApBK9QH4AXDDBt+AI1FKA2gF0AlIcnS5inWpgBvRjG8wloqGxKYEaiAO4wjjIKyqqa2rEwAD6J8Mjo2HiEJKS2TAC+QA


function connect(source: AudioNode, destination: AudioNode | AudioParam) {
    return source.connect(destination);
}

https://www.typescriptlang.org/play/#code/GYVwdgxgLglg9mABBBYCm0AUBnOIBOEaAXIgIIgAm8AcnJWgDSIPaxgCGsCpF1cdBogA+5KvAAKHfBwC2ASkQBvAFCJ1ifGigEkuAkQB0KMOiyt2XeGHkBuFQF8gA

I don't want to typecast or add artificial checks, second case might return different type but Float32Array constructor should definitely work like that.

kirillku commented 4 years ago

I have also encountered this problem when was working with history. It has two overloads for push: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/history/index.d.ts#L15-16

push(path: Path, state?: HistoryLocationState): void;
push(location: LocationDescriptorObject<HistoryLocationState>): void;

I have function getUrl(): Path | LocationDescriptorObject<HistoryLocationState>. And when I am trying to do history.push(getUrl()), I am getting a No overload matches this call. error.

Have to use @ts-ignore for now. Is there any workaround without any extra js code?

Here is a playground with same issue simplified http://www.typescriptlang.org/play/index.html?ssl=1&ssc=1&pln=19&pc=11#code/C4TwDgpgBAglC8UDeUCGAuKBnYAnAlgHYDmANFAEaY4ElQC+A3AFDMD0bUA9gG4S4AbLqgAmWZgDMAroQDGwfF0JQJhABQZseImUoB+atpIBKTDy74RLaXIVKV6zTFNRzllu074AtmAERvCEJgVDtCSRl5RWVVDUxUQhByCgM0RJcEkGRmKChZJSwufwA6IWINUgpjZnpWEQhZAVRcaBso+2IIYBg1FxodKAAfWA9YgHJUMerYlE0JseTMMYoxhmrPKAB3AAss4G38LG5CaEOt3CViPQ38YChvUNltiCOIW+fcFXxcHG5PrAaShE3D4gmEIkk6k63V6xiAA

type A = { a: string, b: string };

// overloads
function fn(a: string, b?: string): void;
function fn(a: A): void;

// implementation
function fn(a: any, b?: any): any {
  console.log(a,b)
}

declare function getA(): string | A;

fn('a')
fn({ a: 'a', b: 'b' })

// why this one is wrong?
// it matches either first or second overload
fn(getA())
huan commented 4 years ago

This issue has pained me for the past years because I have a long override list:

https://github.com/wechaty/wechaty/blob/9edb5529a0aaef32e4794b8362965feeea168153/src/user/message.ts#L419-L424

jcalz commented 3 years ago

Since TS3.0, I think we could do this with a rest parameter whose type is a union of tuple types. For example, given the following multi-call signature type,

type Overloaded = {
  (x: string): number;
  (a: number, b: string): boolean;
}

you should be able to widen it to

type Unified = {
  (...args: [x: string] | [a: number, b: string]): number | boolean;
}

This does seem to behave reasonably:

function foo(x: string): number;
function foo(a: number, b: string): boolean;
function foo(xa: string | number, b?: string) {
  return (typeof xa === "string") ? xa.length : xa === b!.length;
}
const params = Math.random() < 0.5 ? ["a"] as const : [1, "a"] as const;

const f: Overloaded = foo;
f(""); // okay, number
f(0, ""); // okay, boolean
f(...params); // error Expected 1-2 arguments, but got 0 or more. (bizarre error)

const g = foo as Unified;
g(""); // okay, number | boolean
g(0, ""); // okay, number | boolean
g(...params); // okay, number | boolean

If there were only a reasonable way to extract multiple call signatures into a tuple of single-call signatures, then I could even write this myself:

// type Overloads<T> = ... magic to get tuples of call signatures? 
// like https://stackoverflow.com/a/59538756/2887218 but better 

type UnifyOverloads<T extends (...args: any) => any> =
  (...args: Parameters<Overloads<T>[number]>) => ReturnType<Overloads<T>[number]>;

const unifyOverloads = <T extends (...args: any) => any>(f: T) => f as UnifyOverloads<T>;

const val = unifyOverloads(foo)(...params); // okay, number | boolean

(well, you can sort of do this but it doesn't really scale)

Thoughts?

Playground link

Philipp91 commented 3 years ago

So we could come up with a way of trying to collapse overloads

The first 2 overloads can (and should) be combined into a single overload.

I understand that combining overloads into a single one serves as a workaround to this issue (something that TypeScript users do in their code manually). I don't understand why automating this workaround in the TypeScript compiler is seen as a good solution. Is it not possible to relax the TypeScript compiler's desire to pick a single overload when a function is called, and instead let the compiler determine a set of matching overloads?

E.g. in this example, the set of applicable overloads would be empty, so it would be fine to throw the error. But in the initial example, 2 of the 3 overloads would be in the matched set. When the compiler infers the return type, it does so separately for each matched overload and then takes the union of these return types.

maludwig commented 2 years ago

So, since this has been open for, like, a good long while now, maybe, since solving the general case seems difficult difficult lemon difficult, could we maybe just solve the easiest cases? Like where it's like:

function f(numOrString: number);
function f(numOrString: string);

function f(numOrString: number | string) {
  if (typeof numOrString === 'number') {
    return 'its a num';
  } else {
    return 'its a string';
  }
}

function z(numOrString: number | string) {
  return f(numOrString);
}

Is that easier to solve? I bet it would handle like 80% of the problems. Most people don't overload the heck out of functions.

Bessonov commented 2 years ago

@maludwig it's not perfect, but I think there is a rationale behind that:

function f(numOrString: number);
function f(numOrString: string);
function f(numOrString: number | string); // <-- this

function f(numOrString: number | string) {
  if (typeof numOrString === 'number') {
    return 'its a num';
  } else {
    return 'its a string';
  }
}

function z(numOrString: number | string) {
  return f(numOrString);
}

The definition on function:

function f(numOrString: number | string) {

is more like "internal" type and not exposed to outer calls.

Shakeskeyboarde commented 2 years ago

I figured out a recursive way of converting a function overload (function signature intersection) into a union of the individual signatures: Playground link

type OverloadProps<TOverload> = Pick<TOverload, keyof TOverload>;

type OverloadUnionRecursive<TOverload, TPartialOverload = unknown> = TOverload extends (
  ...args: infer TArgs
) => infer TReturn
  ? // Prevent infinite recursion by stopping recursion when TPartialOverload
    // has accumulated all of the TOverload signatures.
    TPartialOverload extends TOverload
    ? never
    :
        | OverloadUnionRecursive<
            TPartialOverload & TOverload,
            TPartialOverload & ((...args: TArgs) => TReturn) & OverloadProps<TOverload>
          >
        | ((...args: TArgs) => TReturn)
  : never;

type OverloadUnion<TOverload extends (...args: any[]) => any> = Exclude<
  OverloadUnionRecursive<
    // The "() => never" signature must be hoisted to the "front" of the
    // intersection, for two reasons: a) because recursion stops when it is
    // encountered, and b) it seems to prevent the collapse of subsequent
    // "compatible" signatures (eg. "() => void" into "(a?: 1) => void"),
    // which gives a direct conversion to a union.
    (() => never) & TOverload
  >,
  TOverload extends () => never ? never : () => never
>;

// Inferring a union of parameter tuples or return types is now possible.
type OverloadParameters<T extends (...args: any[]) => any> = Parameters<OverloadUnion<T>>;
type OverloadReturnType<T extends (...args: any[]) => any> = ReturnType<OverloadUnion<T>>;