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

Allow function overloads with a varying parameter to unify, and inductively narrow the parameter when narrowing the return type #12885

Closed
dead-claudia opened this issue Dec 13, 2016 · 8 comments
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript

Comments

@dead-claudia
Copy link

I don't really know how to word the title for this one or explain it well...but here's what I'm thinking: it would be nice to allow function overloads to unify, provided only one parameter and possibly the return type are different across each overload. Additionally, when narrowing the returned type, it should be able to inductively narrow the overloaded parameter's type accordingly within the same block. To hopefully explain this a little better, here's what I mean:

enum Types { Foo, Bar }
class Foo { foo: number; }
class Bar { bar: number; }
declare function foo(type: Types.Foo, length: number): Foo;
declare function foo(type: Types.Bar, length: number): Bar;

// This should be okay, with `result` inferred as `Foo | Bar`
const type: Types.Foo | Types.Bar = getType();
const result = foo(type, 1);

if (result instanceof Foo) {
    // str is inferred as Types.Foo here
} else {
    // str is inferred as Types.Bar here
}

In this case, if result is a Foo, str can only possibly be a "foo" through induction, and similarly result being a Bar and str being a "bar".

With #12883 also, this would also permit assertions to do compile-time type narrowing, without adding any new syntax or special casing of any particular identifier, hence fixing #12825 and #8655 simultaneously while remaining much more flexible:

declare function assert(cond: true, message: string): void;
declare function assert(cond: false, message: string): never;

I know this would likely be really hard to implement, but it would pay off. It helps that boolean is equivalent to true | false, "foo" is a subtype of string, E is equivalent to E.A | E.B | E.C where enum E {A, B, C}, etc., so much of the structural narrowing would allow this to apply to several other areas.


Note that this would specifically not allow more than one varying type to be unified, because it would be an M⨯N type implication, which would be unrealistic to infer in practice (you would already need length to be similarly guarded in theory):

declare function foo(type: "foo", length: 1): Foo;
declare function foo(type: "bar", length: 2): Foo;

let type: "foo" | "bar" = someType;
let length: 1 | 2 = someLength;
let result = foo(type, length); // Fail

Sorry if I did a really poor job explaining it (I don't really know the correct technical term for this, hence the detailed examples).

@dead-claudia
Copy link
Author

Yeah...I've picked up a few things on how partial types, readonly types, _.pluck, among others, were all fixed with two new primitives: index types and mapped types.

Oh, and this also would fix these two (among likely others):

If type unions of functions are similarly unified, this would also fix #10620 (among likely others).

Similar induction with objects would also fix #12448, but that's beyond the scope of this proposal.

@RyanCavanaugh
Copy link
Member

Duplicate #7294 ?

@mhegazy
Copy link
Contributor

mhegazy commented Dec 14, 2016

I believe these are two issues. 1. issues like String.split (#5766), where multiple overloads only differ in type of one parameter, and thus could not be called with a union type argument. and 2. calling signatures on a union type as (#7294).

In DefinitelyTyped, we have added a tslint rule to flag these cases as an error (see unifiedSignaturesRule). We could incorporate this into the compiler, and make it an error to have two signatures vary only by the type of one paramter, but this would be a breaking change.

@mhegazy
Copy link
Contributor

mhegazy commented Dec 14, 2016

just to clarify what i meant:

interface AcceptsStringAndNumber {
  push(a: string): void;
  push(a: number): void;
}

interface AcceptsString {
  push(a: string): void;
}

interface AcceptsNumber {
  push(a: number): void;
}

type AcceptsStringOrNumber = AcceptsString | AcceptsNumber;


var value: string | number;

var acceptsStringAndNumber: AcceptsStringAndNumber;
acceptsStringAndNumber.push(value); // Safe, but not allowed

var acceptsStringOrNumber: AcceptsStringOrNumber;
acceptsStringOrNumber.push(value); // Unsafe, and not allowed

@dead-claudia
Copy link
Author

dead-claudia commented Dec 15, 2016 via email

@dead-claudia
Copy link
Author

@mhegazy

To clarify the return type half and its related inference, it's this:

declare const stringOrNumber: string | number;

declare function convert(s: string): string;
declare function convert(n: number): number;

const result = convert(stringOrNumber);

if (typeof result === "string") {
    // stringOrNumber is type `string` here
} else {
    // stringOrNumber is type `number` here
}

This was referenced Dec 27, 2016
@falsandtru
Copy link
Contributor

related: #6160

@dead-claudia
Copy link
Author

Closing in favor of #13257, since that is a better thought out, more general superset of this.

@microsoft microsoft locked and limited conversation to collaborators Jun 19, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

4 participants