TypeScript: Promise> cannot exist in JS

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

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Reactions: 30
  • Comments: 18 (8 by maintainers)

Commits related to this issue

Most upvoted comments

@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>>> //...

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>

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
    >

@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>
}

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.

So what is the status of this? It is a real deal-braker for function composition. You can’t do any composition of functions that returns promises because you end with Promise<Promise<Promise<T>>> which doesn’t make any sense in JS

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?

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>.

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