Typescript: Generic parameter inference for Promise.then oddity

Created on 21 Sep 2016  路  2Comments  路  Source: microsoft/TypeScript

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:

  • moving line 5 to line 8 - declare let x below declare function f and g.
  • change of the type of x to Thenable<number>.
  • add a field of type T to Thenable - a: T, that would make Thenable strictly covariant.
  • replace with ternary expression - (_) => (Math.random() < 0.5) ? f() : g()
Breaking Change Bug Fixed

Most helpful comment

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.

All 2 comments

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.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

wmaurer picture wmaurer  路  3Comments

dlaberge picture dlaberge  路  3Comments

uber5001 picture uber5001  路  3Comments

manekinekko picture manekinekko  路  3Comments

remojansen picture remojansen  路  3Comments