Repository navigation
Overload gets lost in mapped type with conditional type #29732
Description
Activity
jack-williams commented
on Feb 4, 2019 CollaboratorMore actionsWhen inferring from a type with multiple call signatures (such as the type of an overloaded function), inferences are made from the last signature (which, presumably, is the most permissive catch-all case). It is not possible to perform overload resolution based on a list of argument types (this would require us to support typeof for arbitrary expressions, as suggested in #6606, or something similar).
From #21496. I suspect this is probably a design limitation.
felixfbecker commented
on Feb 4, 2019 ContributorAuthorMore actionsBut that was before we had tuple types. This overload:
interface Overloads { foo(a: string): void; foo(a: number, b: string): void; }
is equivalent to
interface NoOverloads { foo(...args: [string] | [number, string]): void; }
which works correctly. I would use that workaround, but I don't have control over libraries that use overloads. I would at least expect the compiler to treat the above interface equivalently to the below.
If the return type is different, it's also possible with conditional types:
interface Overloads { foo(a: string): string; foo(a: number, b: string): number; }
interface NoOverloads { foo<P>(...params: P): P extends [number, string] ? number : string; }
So I don't see a reason why overloads would still be impossible for the compiler to handle
Reacted by iamolegga, Nick Ribal, Bruno Lemos, ikokostya, Wolfgang Rathgeb, Alexej Liebenthal, Karen Grigoryan, owl from hogvarts, Homa Wong, Steven Luscher and 4 moreReacted by SlurpTheojack-williams commented
on Feb 4, 2019 CollaboratorMore actionsThe tuple approach seems plausible; perhaps that could be a proposal included in the issue?
The conditional type approach elicits further problems because generic parameters do not get unified.
Reacted by Qwerty (Vítězslav Ackermann Ferko)- addedSuggestionAn idea for TypeScriptAn idea for TypeScriptIn DiscussionNot yet reached consensusNot yet reached consensus
on Feb 5, 2019 I would just like to point out that
interface NoOverloads { foo(...args: [string] | [number, string]): void; } const Bar: NoOverloads = { foo: function (a, b) { return; } }
actually does not work currently.
Check it out on the playground - a and b both get the typenumber | stringI'm personally very interested in seeing typeable overloads. I'm struggling to get TypeScript to accept my current typings for one of my projects: here it is.
Reacted by Paul Grau, SlurpTheo, Brandon Bennett, Qwerty (Vítězslav Ackermann Ferko) and NoraNot sure if it's the same issue but I came here to report this behavior:
type Post = { body: string } type Member = { name: string } type methods = { on(event: 'new_post', payload: Post): void on(event: 'member_add', payload: Member): void } type Params = Parameters<methods['on']> // Expected Params type: ["new_post", Post] | ["member_add", Member] // Current behavior: ["member_add", Member] (only gets the last one)
Related use case: probot/probot#858
Reacted by varHarrie, iamolegga, Nick Ribal, Kevin Sullivan, Abhisek, Alec Larson, Henning Dieterichs, Dimitri Huin, Roman, KennHuang and 15 more- addedNeeds InvestigationThis issue needs a team member to investigate its status.This issue needs a team member to investigate its status.and removedIn DiscussionNot yet reached consensusNot yet reached consensus
on Apr 2, 2019 - addedIn DiscussionNot yet reached consensusNot yet reached consensusand removedNeeds InvestigationThis issue needs a team member to investigate its status.This issue needs a team member to investigate its status.
on Jul 16, 2019 Any news on this? Just hit this bug myself.
Reacted by ikokostya, Philip Diffenderfer, Chad Fong, Wolfgang Rathgeb, Seth Westphal, Steven Luscher, Jarrod Davis, Elijah Olmos, Jonas Eiselt, Matt W and 1 moreClickerMonkey commented
on Dec 31, 2020 More actionsThis limitation has been a thorn in my side for multiple projects. Can anything be done to assist?
Reacted by Chad Fong, Wolfgang Rathgeb, Seth Westphal, timmydoza, Steven Luscher, Jarrod Davis, Elijah Olmos, Max Patiiuk, Matt W and Qwerty (Vítězslav Ackermann Ferko)dragomirtitian commented
on Jan 27, 2021 ContributorMore actionsPhilip Diffenderfer (@ClickerMonkey) For non generic overloaded functions you can use the approach described here to improve the situation
Reacted by Max PatiiukI stumbled upon this issue while trying to generate generic overloads programmatically. Here's a workaround:
Unions are not treated like overloads, but intersections are. So, we just need to find a way to create an intersection of interfaces containing the overloads. We can do that by wrapping the function signatures in interfaces.
Reacted by Reinis Vesers, Lukas Bombach and Eugene ZaretskiyThank you Paul Grau (@graup) for the workaround and the example
I made your Playground simpler so it's easier to follow the idea:
Reacted by Paul GrauBut that was before we had tuple types.
Hi Felix Becker (@felixfbecker)!
Yep~. while inference works, sometime is so complicated that overload is a much better approach. e.g., I'm working on this type:// still work in progress, I'm trying to further simplify it. export type SetState<T> = undefined extends T ? { (value: T | ((draft: T) => T | void), meta?: { logger?: Logger }): T, (value: undefined | typeof nothing | ((draft: T) => typeof nothing), meta?: { logger?: Logger }): undefined, (value: (draft: T) => Promise<T | void>, meta?: { logger?: Logger }): Promise<T>, (value: (draft: T) => Promise<typeof nothing>, meta?: { logger?: Logger }): Promise<undefined> } : { (value: T | ((draft: T) => T | void), meta?: { logger?: Logger }): T, (value: (draft: T) => Promise<T | void>, meta?: { logger?: Logger }): Promise<T>, }
It is very difficult to convert that to use only inference. 🍻
Since we can't use tuple in rest param type, I don't think we can use conditional type to replace function overloads.
e.g.
type F = { () => number, (value: string) => string } // vs type F = <P extends Array<string>>(...args: P): P extends [] ? number : (P extends [string] ? string : never) // this won't work <P extends [] | [string]>(...args: P): ... let f: F f('a', 'b') // function overload prevents this, but conditional type can't
also, user can't easily see what params
f()can take until they try it out.
From the type annotation it just show as(...args: P): ....
User have to read through the whole conditional type and understand what it means,
and the arguments are named asarg_0,arg_1etc.As compare to function overload where you can name the arguments properly.
Arrived here from the same core issue, but with a different use case.
// reduced script type SplitParameter = Parameters<string['split']>[0]; function split(source: string, by: SplitParameter): string[] { return source.split(by); }
This only found the ES2015Splitter variant of
string.split.Checking the rest of the comments, I ended up doing a union, but it still failed the overloads:
type ES5Delimiter = string | RegExp; type ES2015Splitter = { [Symbol.split](string: string, limit?: number): string[]; }; function split(source: string, by: ES5Delimiter|ES2015Splitter): string[] { // @ts-expect-error this fails both overloads :( return source.split(by); } // completely superfluous guards function safeSplit(source: string, by: ES5Delimiter|ES2015Splitter): string[] { if (by instanceof RegExp || typeof by === 'string') { return source.split(by); } return source.split(by); }
I do have a solution, if anyone is interested in a better solution. Existing ones do things like assume there's only 4 overloads, turn it into a union, remove the props on the functions, or many other things that break the fidelity of the overloads.
Here's a basic type that gets the overload signatures of a function:
interface FunctionSignature { parameters: readonly unknown[]; returnType: unknown; } // Returns a `FunctionSignature[]`. type GetFunctionOverloads< T extends AnyFunction > = _GetFunctionOverloads<(() => typeof done) & T>; type _GetFunctionOverloads< T extends AnyFunction, Shape extends object = ToObject<T>, // This would be the first part to change if you want to change the return type. Signatures extends FunctionSignature[] = [], > = T extends AddSignature<Shape, infer Params, infer Return> ? Return extends typeof done ? _GetFunctionOverloads< T, AddSignature<Shape, Params, Return>, // This would be the second. [{ parameters: Params, returnType: Return }, ...Signatures] > : Signatures : Signatures;
Of course this doesn't actually modify the signatures, just return them. My playground goes into some examples of actually mapping the overload signatures and it's pretty thoroughly documented!
Reacted by ipcjs, EqualMa and Take-JohnReacted by Qwerty (Vítězslav Ackermann Ferko)I also made it work via:
type Parameters2<T extends { (arg: any, ...args: any): any; (): any }> = T extends { (arg: infer F, ...args: infer P): any; (): any } ? F | P : never;
For example, with
JQuery<HTMLElement>.textreturns a union of both overloads:
JQuery<HTMLElement>.text(text_function: string | number | boolean | ((this: HTMLDivElement, index: number, text: string) => string | number | boolean)): JQuery<HTMLElement>
and
JQuery<HTMLElement>.text(): stringWhich becomes
string | number | boolean | [] | ((this: HTMLDivElement, index: number, text: string) => string | number | boolean)And all that needs to be done is
Exclude<Parameters2<typeof $.text>, []>Probably not useful for bigger overloads, but exactly what I need here.
ackvf commented
on Dec 31, 2025 More actionsfunction apply(v1: number, v2: number): number; function apply(v1: string, v2: string): string; function apply(v1: boolean, v2: boolean): boolean; function apply(v1: any, v2: any): any { return v1 + v2; } const r1 /* number */ = apply(1, 2) const r2 /* string */ = apply("a", "b") const r3 /* never */ = apply(1, "b") // <- gives error as expected type P0 = typeof apply // { (v1: number, v2: number): number; (v1: string, v2: string): string; (v1: boolean, v2: boolean): boolean; } type P1 = Parameters<P0> // [v1: boolean, v2: boolean] <- only the last overload returned? type OverloadedParameters<T extends (...args: any) => any> = T extends { (...args: infer A): any; // number (...args: infer B): any; // number (...args: infer C): any; // number (...args: infer D): any; // number <- not sure why it is padded with the first type (...args: infer E): any; // string (...args: infer F): any; // boolean } ? A | B | C | D | E | F : never type P2 = OverloadedParameters<P0> // [v1: boolean, v2: boolean] | [v1: number, v2: number] | [v1: string, v2: string] function apply2(...args: OverloadedParameters<P0>) { return apply(...args); // <- error here, returns `never` } const r4 /* never */ = apply2(1, 2); const r5 /* never */ = apply2("a", "b"); const r6 /* never */ = apply2(1, "b"); // <- gives error as expected
It works for the parameters, but return type won't get inferred.
My only wish is that it was possible to exclude an overload similar to
Omit<Type, Keys>orExclude<Union, Members>- with that it would be possible to implement dynamic version of the type.related:
TypeScript Version: 3.4.0-dev.201xxxxx
Search Terms: mapped type overload
Code
Expected behavior:
No error, overload should be maintained.
This makes it impossible to use this pattern with popular types that contain overloads, like Rx
Observablepipe()/subscribe().The
ProxiedObjecttype is used in https://github.com/GoogleChromeLabs/comlink.Actual behavior:
Overload gets lost, compile error when trying to call the first overload.
Playground Link: link