Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Overload gets lost in mapped type with conditional type #29732

Open
felixfbecker opened this issue Feb 4, 2019 · 13 comments
Open

Overload gets lost in mapped type with conditional type #29732

felixfbecker opened this issue Feb 4, 2019 · 13 comments
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript

Comments

@felixfbecker
Copy link
Contributor

felixfbecker commented Feb 4, 2019

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

@jack-williams
Copy link
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.

@felixfbecker
Copy link
Contributor Author

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

@jack-williams
Copy link
Collaborator

jack-williams commented Feb 4, 2019

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.

@RyanCavanaugh RyanCavanaugh added Suggestion An idea for TypeScript In Discussion Not yet reached consensus labels Feb 5, 2019
@nixxquality
Copy link

nixxquality commented Feb 16, 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 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.

@brunolemos
Copy link

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

@rhyek
Copy link

rhyek commented Oct 1, 2020

Any news on this? Just hit this bug myself.

@ClickerMonkey
Copy link

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

@dragomirtitian
Copy link
Contributor

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

@graup
Copy link

graup commented Jul 17, 2021

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

@LukasBombach
Copy link

Thank you @graup for the workaround and the example

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

Simplified Playground example

@unional
Copy link
Contributor

unional commented Oct 29, 2022

A very simple example

But that was before we had tuple types.

Hi @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. 🍻

@unional
Copy link
Contributor

unional commented Nov 19, 2022

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.

@Khez
Copy link

Khez commented Dec 18, 2024

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);
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests