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

Support overload resolution with type union arguments #14107

Open
johnendev opened this issue Feb 16, 2017 · 33 comments
Open

Support overload resolution with type union arguments #14107

johnendev opened this issue Feb 16, 2017 · 33 comments
Labels
Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript

Comments

@johnendev
Copy link

johnendev commented Feb 16, 2017

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
Copy link
Member

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.

@DanielRosenwasser DanielRosenwasser added Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript labels Feb 16, 2017
@johnendev
Copy link
Author

johnendev commented Feb 16, 2017

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
Copy link
Member

#1805 was the prior incarnation of this

@masaeedu
Copy link
Contributor

masaeedu commented Apr 21, 2017

@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
Copy link
Contributor

jcalz commented Jun 29, 2017

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
Copy link

aj-r commented Jul 27, 2017

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
Copy link

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
Copy link
Contributor

saschanaz commented Apr 14, 2018

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 microsoft/TypeScript-DOM-lib-generator#432 (comment)

@maludwig
Copy link

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
Copy link

Bessonov commented May 13, 2022

@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
Copy link

Shakeskeyboarde commented Jun 5, 2022

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

MiyamotoSatoshi added a commit to MiyamotoSatoshi/react that referenced this issue Aug 15, 2023
* #248 Add union overload to ThunkDispatch

See discussion in #248 and microsoft/TypeScript#14107. Without this explicit overload, TypeScript is unable to figure out that the function can be called with an argument of type `T|ThunkAction<...>`.

* Merge ThunkDispatch union overload with renamed type parameters

Co-Authored-By: Tim Dorr <timdorr@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests