Typescript: Promise<Promise<T>> cannot exist in JS

Created on 11 Oct 2018  路  16Comments  路  Source: microsoft/TypeScript


TypeScript Version: Version 3.2.0-dev.20181011


Search Terms:
is:open label:Bug promise
label:Bug wrapped promise
label:Bug wrapped nested

Code

const p1 = new Promise<Promise<Number>>((resolveOuter) => {
    const innerPromise = new Promise<Number>((resolveInner) => {
        console.log('Resolving inner promise')
        resolveInner(1)
    })

    console.log('Resolving outer promise')
    resolveOuter(innerPromise)
})

p1.then((p2: Promise<Number>) => {
    p2.then((num) => 
        console.log('the number is: ', num)
    )
})

Expected behavior:
Compilation should fail, because p1 is actually a Promise<number> due to promise unwrapping.
Actual behavior:
Compilation should fail, requiring code which looks like:

const p1 = new Promise<Number>((resolveOuter) => {
    const innerPromise = new Promise<Number>((resolveInner) => {
        console.log('Resolving inner promise')
        resolveInner(1)
    })

    console.log('Resolving outer promise')
    resolveOuter(innerPromise)
})

p1.then((p2: Number) => {
    console.log('the number is: ', p2)
})

Playground Link:
Runtime error
No runtime error

Related Issues:
Didn't find related issue

lib.d.ts In Discussion Suggestion

Most helpful comment

@ORESoftware It does not matter how often you nest a Promise, the result is the same:

const prom = new Promise((r) => r(new Promise((r2) => r2(42))));
prom.then((x) => console.log(x)); //prints 42
console.log(await prom); //prints 42

This is just how Promises work. You simply cannot get the "inner" Promise, it's gone, flattened.

I'm not saying that the type Promise<Promise<T>> is wrong, it might just be the result of a function, wrapping something into a Promise. However, TypeScript should always collapse nested Promises when type checking and compiling because that is what JavaScript does.

Promise<T> === Promise<Promise<T>> === Promise<Promise<Promise<T>>> //...

All 16 comments

@rbuckton do we have an issue tracking this?

All I have is #17077 and the design notes at #17621, #18155, #19169, #20724.

Seems like you want something like negated types (#18280 or #4196).

Seems like a legit bug, but as a side note, a little strange to resolve a promise from a promise executor callback. Promise executor should basically be reduced down to the lowest level that isn't already promisified. If you already have a promise in the executor, then you could just make that your promise call, instead of creating a wrapper promise around a promise.

We have the same problem. We wrap unknown return types into an additional Promise and can get stuck with a resolved type of Promise<Promise<T>> if the original type was a Promise.

This nested Promise however is problematic:

async function xxx(x: Promise<Promise<string>>): Promise<void> {
  x.then((y) => {
    y.match(/foo/); //invalid
  });

  const z = await x;
  z.match(/foo/); //valid
}

Here, y is wrongfully seen as a Promise<string> while z is correctly seen as string.

Why does await and then behave differently?

Also why can't we assign a Promise<Promise<T>> to a Promise<T> variable? This is valid JS since nested Promises always unwrap.

@hcomnetworkers Promise<Promise<T>> can exist, it looks like this

const resolvesToPromise = function(){
  return new Promise(r => r(new Promise(...));
}

it doesn't get unwrapped in this case since the executor resolves synchronously

@ORESoftware It does not matter how often you nest a Promise, the result is the same:

const prom = new Promise((r) => r(new Promise((r2) => r2(42))));
prom.then((x) => console.log(x)); //prints 42
console.log(await prom); //prints 42

This is just how Promises work. You simply cannot get the "inner" Promise, it's gone, flattened.

I'm not saying that the type Promise<Promise<T>> is wrong, it might just be the result of a function, wrapping something into a Promise. However, TypeScript should always collapse nested Promises when type checking and compiling because that is what JavaScript does.

Promise<T> === Promise<Promise<T>> === Promise<Promise<Promise<T>>> //...

The Promise type seems broken:

playground

const a = Promise.resolve()
    .then<Promise<void>, never>(() => Promise.resolve());

// b is undefined, but its type is Promise<void>
a.then(b => console.log(b));

@hcomnetworkers you're right, never would have guessed that wrt to the promise executor

I encountered this error when writing a higher-order wrapping function

async function asf(s: string) {
        return s
    }

const wrap = <T extends (...args: any[]) => any>(f: T) => {
    return async function(this: any, ...args: Parameters<T>) {
        const r: ReturnType<T> = f.apply(this, args)
        // do something with r
        return r
    }
}

const wrappedFunc = rest(asf) // (this: any, s: string) => Promise<Promise<string>>
const ret = wrappedFunc('string') // TS gives Promise<Promise<string>> here

I have to do

return async function(this: any, ...args: Parameters<T>) {
        const r: ReturnType<T> = f.apply(this, args)
        return r
    } as T

to get ret typed Promise<string>, but I'd rather not use as

@DanielRosenwasser @weswigham @rbuckton would this suffice in lib.es2015.promise.d.ts? It's a bit unintuitive (the type of new Promise<Promise<number>>() would actually evaluate to Promise<number>), but I believe this provides the desired compile-time error.

Playground link with some additional testing/assertions 馃檪

export type FlattenedPromise<T> = 
    unknown extends T 
      ? Promise<T> 
      : T extends Promise<infer U> 
        ? T
        : Promise<T>;

interface PromiseConstructor {
// <snip>
    new <T>(executor: (resolve: (value?: T | PromiseLike<T>) => void, reject: (reason?: any) => void) => void): FlattenedPromise<T>;
// <snip>
}

https://github.com/microsoft/TypeScript/pull/35998#issuecomment-594809232 will make Promise<Promise<T>> assignable to Promise<T>:

This last commit adds the capability to measure a type parameter to determine whether it is awaited in a then method, and if so unwraps the awaited during assignability checks between two identical type references. What this means is that when comparing assignability between two Promise instantiations, the type argument of each promise is first unwrapped before assignability is compared, so Promise<awaited T> is assignable to Promise<T>, and Promise<Promise<Promise<T>>> is now _also_ assignable to Promise<T>.

We are pushing awaited until after TS3.9 while we continue to assess the impact of the change. As a result, I am reopening this issue until we have a solution in place.

The solution would be for Promise not to re-wrap if it's already a Promise. That way, we can never end up with nested Promises. Here's a little proof of concept:

export type Promise<A extends any> =
    globalThis.Promise<
        A extends globalThis.Promise<infer X>
        ? X
        : A
    >

So now we can do:

type t0 = Promise<1>                    // Promise<1>
type t1 = Promise<Promise<1>>           // Promise<1>
type t2 = Promise<Promise<Promise<1>>>  // Promise<1>

awaited keyword was in the 4.0 iteration plan (#38510), but 4.0 just released without it.

I can't find any more discussion about it. It's not in the 4.1 iteration plan (#40124), nor any Design Notes.

I found it being mentioned in #40002, but that pr didn't actually add the Awaited type into libs right?

What's the current state of this issue?

40006 mentions it, and #40002 mentions it. @ahejlsberg @RyanCavanaugh we should determine whether the Awaited type alias makes sense for 4.1.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

kyasbal-1994 picture kyasbal-1994  路  3Comments

jbondc picture jbondc  路  3Comments

bgrieder picture bgrieder  路  3Comments

DanielRosenwasser picture DanielRosenwasser  路  3Comments

siddjain picture siddjain  路  3Comments