microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
100.46k stars 12.42k forks source link

Nested Promise not automatically unwrapped #59111

Open eps1lon opened 3 months ago

eps1lon commented 3 months ago

🔎 Search Terms

return type promise union

🕗 Version & Regression Information

This is the behavior in every version I tried, and I reviewed the FAQ for entries about Promise

⏯ Playground Link

https://www.typescriptlang.org/play/?#code/C4TwDgpgBAyg9gWwgOTgE2gXigZ2AJwEsA7AcygB8oAFfRQnCAHjyLID4BYAKB4EMcIYgGMoAMwCuI4ITjEo+CMQz4AwgAtCAGzQAKAJQAuGnQQNm8JKgzsoAbx5QnCiMAn55teowB0inHBaAG4QugBEwBB4Yfo8AL48PJLSsvKKyhD4AIJgYLrCmjrpxpYo6BD69oncCbzcAPT1sOpwEjpQoJAFEMIA1lAARhLAUJl0+DiGPI1OACrg0ADkXmaMLAQkpOyLUAxQxHAjAjiEpMR8A1rQwHAdC1CLrJuLProATADMACwArLHc6RUOTygMyGm0en0-xmzlhAD0APw8IA

💻 Code

type SomeNode = string | Promise<string>

async function renderChild(): Promise<SomeNode> {
    return Promise.resolve("test")
}

function renderApp(children: SomeNode) {

}

// Should typecheck but errors:
//  Type 'Promise<string>' is not assignable to type 'string'.(2345)
renderApp(renderChild())
//        ^?

🙁 Actual behavior

Argument of type 'Promise<SomeNode>' is not assignable to parameter of type 'SomeNode'.
  Type 'Promise<SomeNode>' is not assignable to type 'Promise<string>'.
    Type 'SomeNode' is not assignable to type 'string'.
      Type 'Promise<string>' is not assignable to type 'string'.(2345)
input.tsx(13, 11): Did you forget to use 'await'?

🙂 Expected behavior

No typechecking error

Additional information about the issue

Nodes in React are typed as type Node = AwaitedNodes | Promise<AwaitedNodes>. However, the return type of async Components is Promise<Node>. But TypeScript will not consider this a Component because Promise<Node> is not assignable to Node even though it should be since at runtime Promise<Promise<T>> will never be observable and always collapse to Promise<T>.

Users can either fix this by using an unwieldy Promise<Awaited<ReactNode>>.

We can also fix this at the type level by allowing Node | Promise<Node> as the return type. Though I suspect this just pushes the issue one Promise-wrapping level away when TypeScript could collapse wrapped Promises automatically.

Original issue: https://github.com/vercel/next.js/discussions/67365

DanielRosenwasser commented 3 months ago

We can also fix this at the type level by allowing Node | Promise<Node> as the return type.

I think this is probably what I'd recommend. I believe @rbuckton did explore something where Promise<T> could be compatible with T | Promise<T>; but I don't know if it would have handled the more recursive case you have up there.

rbuckton commented 2 months ago

You can't actually have a Promise<Promise<T>> in JS, but we have no mechanism to recursively unwrap a type parameter on instantiation. I looked into this a few years ago in #37615 (and #37540), but that solution depended on introducing magic inference rules for Promise-likes that couldn't be applied generally to other types.