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

2.1 breaks promise chaining #12886

Closed
adrienverge opened this issue Dec 13, 2016 · 5 comments · Fixed by #13487
Closed

2.1 breaks promise chaining #12886

adrienverge opened this issue Dec 13, 2016 · 5 comments · Fixed by #13487
Assignees
Labels
Bug A bug in TypeScript Fixed A PR has been merged for this issue

Comments

@adrienverge
Copy link

This (apparently valid) code compiled OK with TypeScript 2.0:

function f(): Promise<void> {
  return Promise.resolve(4).then((n: number) => {
    if (n !== 200) {
      return Promise.reject('error');
    }
    return Promise.resolve();
  });
}

With TypeScript 2.1.4 it gives:

test.ts(2,10): error TS2322: Type 'Promise' is not assignable to type 'Promise'.
Type 'number' is not assignable to type 'void'.

tsconfig.json:

{
  "compilerOptions": {
    "target": "es5",
    "lib": ["es2015"]
  }
}

It seems that the problem comes when promises are chained. For instance when removing the .then() chain, compilation runs without any error:

function f(n: number): Promise<void> {
  if (n !== 200) {
    return Promise.reject('error');
  }
  return Promise.resolve();
}
@HerringtonDarkholme
Copy link
Contributor

HerringtonDarkholme commented Dec 14, 2016

The problem here is how function return type is inferred.

An even reduced example.

declare var rejected: Promise<never>
declare var resolved: Promise<void>
function Then(n: number) {  // inferred as Promise<never>, so it is a subtype of `Promise<number>`
  return n !== 200 ? rejected : resolved
}

var a: Promise<void> = Promise.resolve(4).then(Then); // error here because then resolves to Promise<number>

Current workaround is annotate your function return type to Promise<void>.

The problem may be function signature with void and never as argument and return type. Curiously, if we can add a type tag, just like nominal-type brand, it can resolve the problem. Since type parameter T is covariant in Promise and TypeScript's property is covariant by default, the type tag does help inference.

interface Promise<T> {
  '@@__typeTag': T
  /// other signatures ...
}

@rbuckton what's your opinion?

@DanielRosenwasser
Copy link
Member

DanielRosenwasser commented Dec 14, 2016

This is odd - if I write the following:

var x: Promise<void>
var y: Promise<never>

var a = Math.random() ? x : y;

the type of a is Promise<void>, and I believe the function should be doing the same.

@DanielRosenwasser DanielRosenwasser added the Bug A bug in TypeScript label Dec 14, 2016
@HerringtonDarkholme
Copy link
Contributor

HerringtonDarkholme commented Dec 15, 2016

@DanielRosenwasser
It seems the problem is assignability for never and void in return/parameter. For example

var x: Promise<void>
var y: Promise<never>
x = y
y = x

The code above compiles, Promise<void> and Promise<never> are mutually assignable. So in the conditional ternary, the first operand type is preferred as the result of subtype reduction. If you flip the position of x and y, like var a = Math.random() ? y : x;, a is inferred as Promise<never>. This is a reasonable behavior.

Promise at type level is effectively a collection of function type. So I reduced the problem down to this.

type NeverFunc = (f: (t: never) => never ) => NeverFunc
type VoidFunc = (f: (t: void) => void) => VoidFunc
var a: NeverFunc
var b: VoidFunc

a = b
b = a

It compiles because of bivariance, which is the known old problem all the way back to #1394, #10717.

I wonder whether the type tag approach mentioned above can mitigate this by guaranteeing covariant behavior. Yet I'm happier to see strict variance can come to TypeScript.

@raijinsetsu
Copy link

I'm having a similar issue which I am not able to work-around with any of the above information.

interface TypeA {
  a: string;
}

interface TypeB {
  b: string;
}

declare function fnA(): Promise<TypeA>;
declare function fnB(a: TypeA): Promise<TypeB>;

fnA()
  .then((instanceOfA) => fnB(instanceOfA))
  .then((instanceOfB) => {
    console.log(instanceOfB.b);
  });

The compiler complains on the console.log call because 'instanceOfB' is identified as being TypeA which does not have member 'b'. Additionally, VSCode also sees the issue (which makes sense).

instanceB should get its type signature from the return signature of the value return in the previous then, but it is not.

I even tried EXPLICITLY defining the return type of the function passed to .then, but it still complained:

  .then((instanceOfA): Promise<TypeB> => fnB())

This has made it impossible for us to update to 2.1 without doing nasty stuff like double casting:

    console.log((<TypeB><any>instanceOfB).b);

@tvedtorama
Copy link

tvedtorama commented Jan 12, 2017

@raijinsetsu I have the same problem. I have chains of promises that transforms the information down the line. This compiles fine with tsc 2.0.8, but breaks in 2.1 - in that the compiler insists the type is the same as on the root promise.

I suspect it might be the library definitions, the d.ts file, that is outdated - but not sure how to progress.

Sublime Text with Typescript also complains about this issue, even if I only have tsc 2.0.8 on my machine. This mix of versions makes the code challenging to work with.

Any help on this would be greatly appreciated.

Edit: Seems that this is under investigation in: 10977

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Bug A bug in TypeScript Fixed A PR has been merged for this issue
Projects
None yet
Development

Successfully merging a pull request may close this issue.

7 participants