Skip to content

Types related to ReactNode inferred incorrectly when using React 17 #50659

Closed

Description

TL;DR: Array.map on ReactNode[] outputs something like ({} | null | undefined)[].
Answer: Because ReactNode in React 17 is equivalent to unknown.

“Bug” Report

Bug Report

Search Terms

Array.map, generics, union type, unknown

Version & Regression Information

This is the behavior in every version I tried

Link to Repro

I am not able to change the React version in the playground, so here is the link to CodeSandbox

Code

import type { ReactNode } from "react";
import type { Equal, Expect } from "@type-challenges/utils";

function test1(nodes: ReactNode[], fallback: ReactNode = <div />) {
  return nodes.map((item) => item || fallback);
}

function test2(nodes: ReactNode[], fallback: ReactNode = <div />) {
  return nodes.map((item) => (item === "" ? fallback : item));
}

function test3(nodes: ReactNode[], fallback?: ReactNode) {
  return nodes.map((item) => item || fallback);
}

function test4(nodes: ReactNode[], fallback?: ReactNode) {
  return nodes.map((item) => (item === "" ? fallback : item));
}

type Test1 = Expect<Equal<ReturnType<typeof test1>, ReactNode[]>>;
type Test2 = Expect<Equal<ReturnType<typeof test2>, ReactNode[]>>;
type Test3 = Expect<Equal<ReturnType<typeof test3>, ReactNode[]>>;
type Test4 = Expect<Equal<ReturnType<typeof test4>, ReactNode[]>>;

Actual behavior

In non-strict mode, the return type of test1 and test3 is {}[], and the other two produces ReactNode[].
In strict mode, the return type of test1 is ({} | null)[], test2 and test3 produces ({} | null | undefined)[], and the remaining one produces ReactNode[].

Expected behavior

The return type of all of the 4 functions above being ReactNode[].

React 17, not React 18.

Activity

  1. andrewbranch commented on Sep 9, 2022

    @andrewbranch
    Member

    ReactNode is

    type ReactNode = ReactChild | ReactFragment | ReactPortal | boolean | null | undefined;

    and ReactFragment is {} | Iterable<ReactNode>.

    {} is the supertype of everything except null and undefined. So ReactNode reduces to {} | null | undefined (which is actually interchangeable with unknown in 4.8) when you poke it enough to force subtype reduction, e.g., in narrowing.

    To get a feel for how this works, we can just look at test1. Since fallback has a default value, it actually narrows down from ReactNode. We know that fallback can be any one of the things ReactNode can be, except actually not undefined, since we can tell <div /> is not undefined. So we remove undefined from the possible return type of the map callback:

    - ReactChild | {} | Iterable<ReactNode> | ReactPortal | boolean | null | undefined
    + ReactChild | {} | Iterable<ReactNode> | ReactPortal | boolean | null

    and then do subtype reduction to remove redundant types, which leaves us with {} | null—that is, any possible value except undefined.

    So this is actually a more specific type than ReactNode, which is equivalent to unknown. TypeScript is helping you narrow down what’s actually possible for this function to return. This is a feature. Operating on a pretty useless type, ReactNode. Literally anything can be a ReactNode—don’t let it trick you with its specific-sounding name—so it’s not surprising the return types are also pretty vague.

    You’re free to annotate the return types of your functions as ReactNode if you want them to remain unknown.

  2. graphemecluster commented on Sep 10, 2022

    @graphemecluster
    ContributorAuthor

    Andrew Branch (@andrewbranch) Oh I get it now. I missed the typing of ReactFragment. I should have inspected the type more carefully. A tooltip like type ReactNode = unknown would be helpful, nevertheless.

    So this must be a DefinitelyTyped issue then. Does anyone know how come the definition of ReactNode in React 17 is so loose? (I understand changing this breaks things, but I am still looking for a (historical) reason.)
    Well I guess I’m going to migrate to React 18 when I get some time.

  3. typescript-bot commented on Sep 12, 2022

    @typescript-bot
    Contributor

    This issue has been marked 'Working as Intended' and has seen no recent activity. It has been automatically closed for house-keeping purposes.

  4. locked as resolved and limited conversation to collaborators on Oct 22, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Working as IntendedThe behavior described is the intended behavior; this is not a bug

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions