TypeScript Version: 1.8.0 / nightly (2.0.5-dev.20160919)
Code
interface Thenable<T> {
then<U>(then: (value: T) => U | Thenable<U>): Thenable<U>;
}
declare let x: Thenable<string>;
declare function f(): Thenable<string|null>|null;
declare function g(): Thenable<string>;
let y = x.then((_) => {
if (Math.random() < 0.5) {
return f();
} else {
return g();
}
});
Expected behavior:
U should be inferred to be string | null as that solves the type constraints. It can be verified by explicitly adding then<string | null>.(...) which type checks.
Actual behavior:
TS errors with Argument of type '(_: string) => Thenable<string> | null' is not assignable to parameter of type '(value: string) => string | Thenable<string>'.
Oddities:
The following changes make the error disappear, while I expect them to have no relevance to the type of the closure:
declare let x below declare function f and g.Thenable<number>.a: T, that would make Thenable strictly covariant.(_) => (Math.random() < 0.5) ? f() : g()The core issue here is that Thenable<string> and Thenable<string | null> are both subtypes of each other because within Thenable<T>, T is only used in a bi-variant position (i.e. a parameter position). When two distinct types are both subtypes of each other and the compiler performs subtype reduction (as it does when inferring a return type from multiple return statements), the compiler picks "the first" of the types based on an internal ordering that appears somewhat arbitrary to the external observer. That's why seemingly irrelevant changes in the ordering of declarations produces different outcomes.
This behavior is obviously not desirable. We'll think about possible ways we can introduce tie breaker rules to disambiguate in cases like this.
Ah, that makes total sense, and explains why moving type declarations have effects on the type inference.
Apart from changing the choices in the subtype reduction, I wonder if there is a way to have the algorithm picking U to consider U = string | null even if the type of passed call back is explicitly written as Thenable<string> | null. At least conceptually, such U is a valid solution because string | null | Thenable<string | null> is a supertype of Thenable<string> | null.
Most helpful comment
The core issue here is that
Thenable<string>andThenable<string | null>are both subtypes of each other because withinThenable<T>,Tis only used in a bi-variant position (i.e. a parameter position). When two distinct types are both subtypes of each other and the compiler performs subtype reduction (as it does when inferring a return type from multiplereturnstatements), the compiler picks "the first" of the types based on an internal ordering that appears somewhat arbitrary to the external observer. That's why seemingly irrelevant changes in the ordering of declarations produces different outcomes.This behavior is obviously not desirable. We'll think about possible ways we can introduce tie breaker rules to disambiguate in cases like this.