Repository navigation
Support overload resolution with type union arguments #14107
Description
Activity
DanielRosenwasser commented
on Feb 16, 2017 MemberMore actionsI 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
foosucceed? Probably not, since we can't guarantee thatsn1andsn2share 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
- It seems a little ad-hoc, and ideally we'd like to generalize the behavior.
- That process could be expensive (although this is less of a concern - this could be lazily done if overload resolution fails).
- This process is potentially not as trivial as it sounds at face value.
Reacted by Spencer Bliven, Lemon7, Tobías, Spencer Park, Chigozirim, Emanuel Tesař, IronSpecs, Vladimir Katushenok, Tylor Reynolds, Chris Savvopoulos and 14 moreReacted by Artyom Stepanishchev- addedNeeds ProposalThis issue needs a plan that clarifies the finer details of how it could be implemented.This issue needs a plan that clarifies the finer details of how it could be implemented.SuggestionAn idea for TypeScriptAn idea for TypeScript
on Feb 16, 2017 johnpedersen-freightways commented
on Feb 16, 2017 AuthorMore actionsOk. 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.
Reacted by Zura Benashvili, Tylor Reynolds, Connor White, Yibs2016, Finn Merlett, Peter Flynn and Jimmy ClevelandRyanCavanaugh commented
on Feb 16, 2017 MemberMore actions#1805 was the prior incarnation of this
Daniel Rosenwasser (@DanielRosenwasser) Say we treat functions of the form
(a: A, b: B) => Ridentically to functions of the form(a: A) => (b: B) => Rfor 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 withA1 | 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 argumentA1 | A2beR1 | R2(when invoked withA1, beR1, etc). Note thatR1 | R2is 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 typesB1 & B2, and that it returnS1 | 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:
(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- We apply the argument
string | numberto the function type(s1: string) => S & (n1: number) => Nto obtain the return typeS | N, forS == (s2: string) => voidandN == (n2: number) => number - We try to apply the argument
string | numberto the function type(s2: string) => void | (n2: number) => number, but find that we cannot, since it can only accept a parameter of typestring & number. The program does not succeed type checking - If we had provided
string & numberfor the second argument, type checking would succeed and the overall return type would bevoid | number
The requirement for the second argument to be of type
string & numberintuitively 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.Reacted by Joe Calzaretta, Spencer Bliven, ExE Boss, Misha Kaletsky, Changdae Park and HadiMardanianIt 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!
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 Asad Saeeduddin (@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. Asad Saeeduddin (@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!
I think that this union type argument check should only be done if all of the following apply:
- 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.
- 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.
Reacted by Bartosz, Borek Bernard and IronSpecsRan 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. :/
Reacted by Lemon7, Alexey Gerasimov, Valentin Hervieu, Serhii Holinei, Philipp Keck, Finn Merlett and Changdae Parksaschanaz commented
on Apr 14, 2018 ContributorMore actionsThis is needed to type
FormData.appendcorrectly.interface FormData { append(name: string, value: string): void; append(name: string, blobValue: Blob, filename?: string): void; }
Currently we want to allow
string | Blobunion so we type it asappend(name: string, value: string | Blob, filename?: string): void. This incorrectly allowsappend("str", "str", "str");that throws on Firefox.Reacted by Samuel GausI think this is no longer needed now that we have conditional types.
Kagami Sascha Rosylight (@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; }
Reacted by Lorenz LeutgebI think this is no longer needed now that we have conditional types.
AJ Richardson (@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>'
Ankur Oberoi (@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
voidtoundefined, sincevoid | number | booleanseems 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.
Reacted by Ankur Oberoi, Tobías and Yurui ZhangReacted by TobíasI think you're right that overloads can be re-written as generics + conditionals, but it is very clunky. Even your simple
Fooexample 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
Felipe (@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.
18 remaining items
I have also encountered this problem when was working with
history. It has two overloads forpush:
https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/history/index.d.ts#L15-16push(path: Path, state?: HistoryLocationState): void; push(location: LocationDescriptorObject<HistoryLocationState>): void;
I have
function getUrl(): Path | LocationDescriptorObject<HistoryLocationState>. And when I am trying to dohistory.push(getUrl()), I am getting aNo overload matches this call.error.Have to use
@ts-ignorefor 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+fcFXxcHG5PrAaShE3D4gmEIkk6k63V6xiAAtype 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())
- added a commit that references this issue
on May 20, 2020 This issue has pained me for the past years because I have a long override list:
Reacted by ExE Boss, 流浪大法师, Maciej Holyszko and Ari Rahikkala- added a commit that references this issue
on Aug 25, 2020 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?
Reacted by pqnetSo 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.
Reacted by Andrey Kupreychik, Michael Hixson, Torleif Berger, stefnotch, Samuel Gaus and Matt WSo, 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.
Reacted by Misha and StianMitchell Ludwig (@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.
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>>;
Reacted by 雪霁, Jason Pickens, Matt S, Savva Mikhalevski, trent, Igor Kamyşev, Sravan S, John Fawcett, Rexford Essilfie, Bener and 4 moreReacted by lazytype, Karol Majewski, owl from hogvarts, Daniel Sousa, Rexford Essilfie and HelgardReacted by Sterling Camden, trent, Aleksey Imuzov, John O'Sullivan, Michael Cousins, John Fawcett, Wilco Bakker, Alexey Immoreev, Samuel Stoltenberg, Rexford Essilfie and 3 more- added a commit that references this issue
on Aug 15, 2023 Correct me if I'm wrong, I think this is relevant:
matthieusieben commented
on Jun 13, 2025 More actionsA similar thing happens when working with generics (playground):
interface Foo { a:(x: number) => void b:(x: string) => void } declare var foo: Foo function trigger<M extends keyof Foo>( m: M, args: Parameters<Foo[M]> ) { return foo[m](...args) // Error: ts 2556 }
In this example,
argsis not considered as a "tuple" althoughParameters<Foo[keyof Foo]>is ([x: number] | [x: string])
TypeScript Version: 2.1.6
Code
Expected behavior:
This should be allowed.
The type of
x1andx2should bevoid | 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'.