TypeScript: await Promise.all fails to resolve due to optional chaining in JSX

Bug Report - await Promise.all fails to resolve due to optional chaining inside JSX

I am sorry if this doesn’t end up being a TypeScript bug, I’m just not able to replicate this without React at the moment. This feels more like a TypeScript issue? So I’ll let you guys decide.

🔎 Search Terms

await Promise.all toString JSX ReactNode

🕗 Version & Regression Information

  • This is a type checking error
  • Broken on 4.9.4, works with version 4.6.2
  • I am only able to replicate this with React 17.0.2, not react 18

⏯ Repo Link

Sorry, I have to use a repo for this. It’s a bit of a setup to create the conditions for this. https://github.com/Fish1/bughunter

💻 Code

/src/pages/HomePage.tsx

import { ReactNode } from "react";
import MyList from "../component/MyList";

function HomePage() {

  async function myUnusedFunction() {
      const getDataFetch1 = Promise.resolve(["hello", "world"])
      const getDataFetch2 = Promise.resolve([{ id: 1 }, { id: 2 }])

      const [data1, data2] = await Promise.all([
        getDataFetch1, getDataFetch2 
      ]);

      // *******************************
      // TypeScript thinks data2 is a Promise<{ id: number; }[]>
      const x = data2.map((data) => {
        return data.id.toString();
      });
   
      // *******************************
      // TypeScript thinks data1 is a Promise<string[]>
      const y = data1.map((data) => {
        return data.toString();
      });

      return { x, y };
  }

  const myNodeArray: ReactNode[] = [
    <div>hello</div>,
    <div>world</div>,
    "hello",
    "sup",
  ]

  return (
    <div>
      <h1>Home Page</h1>
      <MyList columnsCurrent={myNodeArray}/>
    </div>
  );
}

export default HomePage;

/src/component/MyList.tsx

import { ReactNode } from "react";

export interface ListThingProps {
  columnsCurrent: ReactNode[],
}

function MyList(props: ListThingProps) {
  const { columnsCurrent } = props;

  return (
    <div>
      {
        columnsCurrent.map((column, index) => {

          /**
           * *******************************************************************
           * This variable existing will cause Promise.all() to not resolve its promises.
           * *******************************************************************
           */
          const brokenString = column?.toString();

          return (
            <div key={index}>
              {/* remove {column} here to fix the Promise.all() error */}
              {column}
            </div>
          )
        })
      }
    </div>
  );
}

export default MyList;

output of npx tsc

src/pages/HomePage.tsx:14:23 - error TS2339: Property 'map' does not exist on type 'Promise<{ id: number; }[]>'.

14       const x = data2.map((data) => {
                         ~~~

  src/pages/HomePage.tsx:14:23
    14       const x = data2.map((data) => {
                             ~~~
    Did you forget to use 'await'?

src/pages/HomePage.tsx:14:28 - error TS7006: Parameter 'data' implicitly has an 'any' type.

14       const x = data2.map((data) => {
                              ~~~~

src/pages/HomePage.tsx:18:23 - error TS2339: Property 'map' does not exist on type 'Promise<string[]>'.

18       const y = data1.map((data) => {
                         ~~~

  src/pages/HomePage.tsx:18:23
    18       const y = data1.map((data) => {
                             ~~~
    Did you forget to use 'await'?

src/pages/HomePage.tsx:18:28 - error TS7006: Parameter 'data' implicitly has an 'any' type.

18       const y = data1.map((data) => {
                              ~~~~


Found 4 errors in the same file, starting at: src/pages/HomePage.tsx:14

🙁 Actual behavior

await Promise.all doesn’t resolve the promises.

🙂 Expected behavior

I expect Promise.all() to resolve it’s types correctly regardless of a variable in another file.

Strange Solutions that I have come up with

  1. Comment out the brokenString variable in /src/component/MyList.tsx
  2. Uncomment the FixPromiseError() function in src/pages/DummyPage.tsx
  3. Remove {column} from inside the div in /src/component/MyList.tsx

Here is a link to the TypeScript community discord, and the thread with a lots more testing. https://discord.com/channels/508357248330760243/1068268321998372964

Video of error (incase text description wasn’t clear)

Video.webm

About this issue

  • Original URL
  • State: closed
  • Created a year ago
  • Reactions: 1
  • Comments: 16 (6 by maintainers)

Most upvoted comments

It was so bizarre that I couldn’t resist debugging this 😅

@RyanCavanaugh Thank you so much! I think I was able to remove the React dependency. It is pushed to the “minimal” branch of my GitHub repo.

Branch: https://github.com/Fish1/bughunter/tree/minimal

/main.tsx

async function myUnusedFunction() {
  const fetch1 = Promise.resolve(['hello', 'world']);
  const [data1] = await Promise.all([fetch1]);
  const y = data1.map((data) => {
    return data.toString();
  });
  return { y };
}

export default myUnusedFunction;

/MyList.tsx

type myType = {} | null | undefined;

function MyList() {
  [null as myType].map(column => {
    column?.toLocaleString;
    const aa = column;
  });
}

export default MyList;

image

OK, so it’s just these two files

import { ReactNode } from "react";

function MyList() {
  [null as ReactNode].map(column => {
    column?.toLocaleString; // <-- key line
    const aa = column; // <-- also key line
  });
}
async function myUnusedFunction() {
  const getDataFetch1 = Promise.resolve(["hello", "world"])
  const [data1] = await Promise.all([
    getDataFetch1 
  ]);
  data1.map(() => { });
}

listed in that order in tsconfig, with these dependencies:

  "dependencies": {
    "react": "17.0.2",
    "react-dom": "17.0.2"
  },
  "devDependencies": {
    "@types/react": "17.0.53",
    "@types/react-dom": "17.0.18",
    "@vitejs/plugin-react": "2.2.0",
    "typescript": "4.9.4",
    "vite": "3.2.5"
  }

I’m incredibly curious what’s going on, but would really want the react dependency stubbed out into something that still repros this before investigating further. This bug, whatever it is, is very hard to hit.

Minimal repro I have so far:

import {} from "../component/MyList";

async function myUnusedFunction() {
  const getDataFetch1 = Promise.resolve(["hello", "world"])
  const [data1] = await Promise.all([getDataFetch1]);
  const y = data1.map((data) => {
    return data.toString();
  });
  return { y };
}
import { ReactNode } from "react";

function MyList() {
  [null as ReactNode].map(column => {
    column?.toLocaleString; // <-- key line
    const aa = column;
  });
}

Trying to simplify further but this is very, very delicate