Skip to content

Overload gets lost in mapped type with conditional type #29732

Description

TypeScript Version: 3.4.0-dev.201xxxxx

Search Terms: mapped type overload

Code

interface Overloads {
  foo(a: string): void;
  foo(a: number, b: string): void;
}

/** Converts all properties of an object to Promises and all methods to return Promises */
type ProxiedObject<T> = {
  [P in keyof T]: T[P] extends (...args: infer Arguments) => infer R
    ? (...args: Arguments) => Promise<R>
    : Promise<T[P]>
};

declare let x: ProxiedObject<Overloads>;
x.foo("abc"); // Error: [ts] Expected 2 arguments, but got 1. [2554]
x.foo(123, "abc");

Expected behavior:
No error, overload should be maintained.

This makes it impossible to use this pattern with popular types that contain overloads, like Rx Observable pipe()/subscribe().
The ProxiedObject type 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

Activity

  1. jack-williams commented on Feb 4, 2019

    @jack-williams
    Collaborator

    When 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.

  2. felixfbecker commented on Feb 4, 2019

    @felixfbecker
    ContributorAuthor

    But 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

  3. jack-williams commented on Feb 4, 2019

    @jack-williams
    Collaborator

    The 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.

  4. nixxquality commented on Feb 16, 2019

    @nixxquality

    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 type number | string

    I'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.

  5. brunolemos commented on Feb 16, 2019

    @brunolemos

    Not 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)

    Playground

    Related use case: probot/probot#858

  6. rhyek commented on Oct 1, 2020

    @rhyek

    Any news on this? Just hit this bug myself.

  7. ClickerMonkey commented on Dec 31, 2020

    @ClickerMonkey

    This limitation has been a thorn in my side for multiple projects. Can anything be done to assist?

  8. dragomirtitian commented on Jan 27, 2021

    @dragomirtitian
    Contributor

    Philip Diffenderfer (@ClickerMonkey) For non generic overloaded functions you can use the approach described here to improve the situation

  9. graup commented on Jul 17, 2021

    @graup

    I 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.

    Playground example

  10. LukasBombach commented on Jan 21, 2022

    @LukasBombach

    Thank you Paul Grau (@graup) for the workaround and the example

    I made your Playground simpler so it's easier to follow the idea:

    Simplified Playground example

  11. unional commented on Oct 29, 2022

    @unional
    Contributor

    A very simple example

    But 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. 🍻

  12. unional commented on Nov 19, 2022

    @unional
    Contributor

    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 as arg_0, arg_1 etc.

    As compare to function overload where you can name the arguments properly.

  13. Khez commented on Dec 18, 2024

    @Khez

    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);
    }
  14. LukeAbby commented on Dec 21, 2024

    @LukeAbby

    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!

  15. thakyZ commented on Oct 4, 2025

    @thakyZ

    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>.text returns 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(): string

    Which 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.

  16. ackvf commented on Dec 31, 2025

    @ackvf
    function 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

    Playground Link

    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> or Exclude<Union, Members> - with that it would be possible to implement dynamic version of the type.

    related:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    In DiscussionNot yet reached consensusSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions