Skip to content

Nested Promise not automatically unwrappedΒ #59111

Description

πŸ”Ž 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: vercel/next.js#67365

Activity

  1. DanielRosenwasser commented on Jul 2, 2024

    @DanielRosenwasser
    Member

    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 Ron Buckton (@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.

  2. rbuckton commented on Jul 9, 2024

    @rbuckton
    Contributor

    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.

  3. eps1lon commented on Dec 9, 2024

    @eps1lon
    ContributorAuthor

    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.

    Going with this approach for now (DefinitelyTyped/DefinitelyTyped#71379).

  4. mzuzcak-ataccama commented on Apr 1, 2025

    @mzuzcak-ataccama

    Sebastian "Sebbie" Silbermann (@eps1lon) i believe that with your change, you broke the types in this kind of code

    const P: React.FC = () => "test";
    function App() {
      return (
        <div>
          {P({})}
        </div>
      );
    }

    this is the error on P invocation:

    Type 'ReactNode | Promise<ReactNode>' is not assignable to type 'ReactNode'.
      Type 'Promise<ReactNode>' is not assignable to type 'ReactNode'.
        Type 'Promise<ReactNode>' is not assignable to type 'Promise<AwaitedReactNode>'.
          Type 'ReactNode' is not assignable to type 'AwaitedReactNode'.
            Type 'Promise<AwaitedReactNode>' is not assignable to type 'AwaitedReactNode'.(2322)
    const P: React.FunctionComponent
    (props: {}) => ReactNode | Promise<ReactNode>
    
  5. Chromosore commented on Aug 21, 2025

    @Chromosore

    Kind of related (the other side of the problem) : this piece of code passes type check but does not work actually :

    function join<T>(outer: Promise<Promise<T>>): Promise<T> {
        const promise = new Promise<T>((resolve) => {
            outer.then(inner => inner.then(value => resolve(value)))
        })
        return promise
    }

    Since the type Promise<Promise<T>> cannot exist in javascript, it makes Promise a special case that would justify custom logic to forbid it IMHO. However, unlike the initial problem that arises in React, the join function is useless so this side of the problem is probably not worth considering.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Needs InvestigationThis issue needs a team member to investigate its status.

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions