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

Merge/narrow down union types when they're called as a function #11940

Closed
thorn0 opened this issue Oct 29, 2016 · 2 comments
Closed

Merge/narrow down union types when they're called as a function #11940

thorn0 opened this issue Oct 29, 2016 · 2 comments
Labels
Duplicate An existing issue was already created

Comments

@thorn0
Copy link

thorn0 commented Oct 29, 2016

Sometimes there is more than one way to write a type definition for a function. In particular, the same types often can be expressed using either union types or overloads. Compare:

declare class A1 {
    foo(arg: number[] | number): string;
}

and

declare class A2 {
    foo(arg: number[]): string;
    foo(arg: number): string;
}

When writing type definitions for libraries, I used to think union types are preferable to overloads. I was sure union types enable for more flexibility. For example, I can't call foo with a parameter of type number[] | number if the class A2 is used:

var a1: A1, a2: A2;
var x: number | number[];
a1.foo(x); // Union types: OK
a2.foo(x); // Overloads: ERROR

For this reasons, in my PRs sent to DefinitelyTyped I always tried to prefer union types unless it was clear that overloads is a better choice (different parameter names for different overloads, different semantics, etc.). However, I've started discovering counter-examples recently:

declare class B {
    foo(arg: number[]): string;
}
var a1b: A1 | B, a2b: A2 | B;
a1b.foo([1]); // Union types: ERROR
a2b.foo([1]); // Overloads: OK

This example is of course artificial, so let me describe a more realistic situation. Imagine we have two implementations of Promises — Q and the promises from Selenium WebDriverJs — and need to use one or another depending on some logic. So we have a promiseImplementation variable:

let promiseImplementation: typeof Q | typeof webdriver.promise;

Both implementations have the all method whose argument can be an array (the type of parameter is specified as (T|Promise<T>)[]). As for the Q library, this argument can also be a promise ((T|Promise<T>)[] | Promise< (T|Promise<T>)[] >). But should it matter if we want to pass an array, which anyway is supported by both libraries? I believe it shouldn't. So let's try:

declare let x: any[];
let result = promiseImplementation.all(x); // ERROR:
// "Cannot invoke an expression whose type lacks a call signature."

No luck! However, if the all method of Q was defined using overloads, it'd work.

So now, I'm completely unsure about what I should prefer when writing type definitions. Both union types and overloads are able to cause unexpected counter-intuitive errors, which make you think 'I wish it was defined the other way around' when you find the reason of those errors. However, a developer who will use the type definitions shouldn't need to care at all about these subtleties.

So, back to my initial example. The type of a1b.foo is:

((arr: number | number[]) => string) | ((arr: number[]) => string)

I'd like to propose that the compiler should be smart enough to understand that this type is callable if it's called with an argument of type number[]. That is the type of a1b.foo would be narrowed down to (arr: number[]) => string when a1b.foo is called as a function. If there has been a previous discussion on this, I apologize. I wasn't able to find it.

TypeScript Version: 2.0.6

@RyanCavanaugh
Copy link
Member

Duplicate of #7294 ?

@thorn0
Copy link
Author

thorn0 commented Oct 31, 2016

Oops. Yes. Missed it.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests

2 participants