1. ホーム
  2. React

【React】ReactNode・ReactElement・JSX.Element の違いと使い分け|children に型を付ける方法

Share

TypeScript で React を書いていると、「JSX を受け取る props にはどの型を書けばいいのか」で必ず一度は迷います。候補として出てくるのが ReactNodeReactElementJSX.Element の3つです。名前が似ているうえに、どれを書いても動く場面があるため、なんとなく選んでしまいがちです。この記事では @types/react の実際の定義をもとに3つの関係を整理し、children には ReactNode、戻り値の型を明示するなら ReactElement、という実務での指針を解説します。

3つの型の関係を先に押さえる

結論から言うと、この3つは並列の選択肢ではありません。ReactNode が最も広く、その中に ReactElement が含まれ、JSX.ElementReactElement のエイリアス(別名)にすぎません。つまり ReactNodeReactElementJSX.Element という包含関係です。

表しているもの主な用途
ReactNodeReact がレンダリングできる値すべて(文字列・数値・boolean・null・undefined・配列・要素・ポータル)children や、JSX を受け取る props の型
ReactElementcreateElement() が返すオブジェクト1個。typepropskey を持つコンポーネントの戻り値の型、要素そのものを加工する処理
JSX.ElementReactElement<any, any> の別名。JSX 式を書いたときに推論される型基本的に ReactElement と同じ。積極的に自分で書く必要はない

この関係を頭に入れておくと、「広い型を要求すれば呼び出し側が自由になり、狭い型を要求すると呼び出し側が窮屈になる」という当たり前の話に落ち着きます。children のように何が渡ってくるか分からない場所で狭い型を書くのが、よくあるつまずきです。

ReactNode はレンダリングできる値すべてを含む

ReactNode は、React が画面に描画できる値をまとめたユニオン型です。@types/react の定義はおおむね次のような形になっています(実験的な内部メンバーは省略しています)。

@types/react(抜粋)
type ReactNode =
  | ReactElement
  | string
  | number
  | bigint          // React 19 向けの型定義で追加
  | Iterable<ReactNode>  // 配列などの反復可能なもの
  | ReactPortal
  | boolean
  | null
  | undefined;

注目したいのは、要素だけでなく文字列や数値、nullundefined、boolean、そして配列まで含まれている点です。これは JSX で実際に書ける内容とぴったり一致します。<Card>テキストだけ</Card> と書けば children は文字列になり、<Card>{items.map(...)}</Card> と書けば配列になり、<Card>{isOpen && <Body />}</Card> と書けば false が入ることもあります。ReactNode はそのすべてを受け止められます。

ちなみに booleannullundefined は「型として許されている」だけで、実際には何も描画されません。{isOpen && <Body />} のような条件付きレンダーが書けるのは、この仕様のおかげです。

そのため、children の型は素直に ReactNode にしておくのが正解です。

Card.tsx
import type { ReactNode } from 'react';

type CardProps = {
  title: string;
  // 何が来ても受け取れるようにする
  children: ReactNode;
};

export function Card({ title, children }: CardProps) {
  return (
    <div className="card">
      <h2>{title}</h2>
      <div className="card__body">{children}</div>
    </div>
  );
}

子要素を省略できるようにしたい場合は children?: ReactNode とオプショナルにします。ただし ReactNode には undefined が含まれているので、strict 有効時に children: ReactNode と必須で書いても <Card title="A" /> のように省略した書き方はエラーになりません。子要素を必ず渡させたいなら children: ReactElement のように狭める必要がありますが、後述の理由でおすすめはしません。

ReactElement は createElement が返すオブジェクトの型

ReactElement は、createElement()(=JSX を書いたときに実行されるもの)が返す1個のオブジェクトを表す型です。DOM 要素そのものではなく、「何を描画してほしいか」を記述した設計図のようなオブジェクトです。定義は次のようになっています。

@types/react(抜粋)
interface ReactElement<
  P = any,
  T extends string | JSXElementConstructor<any> = string | JSXElementConstructor<any>,
> {
  type: T;    // 'div' などのタグ名、またはコンポーネント関数
  props: P;   // 渡された props
  key: string | null;
}

実体を確認するとイメージしやすくなります。JSX を変数に入れて中身を覗くと、typepropskey を持つただのオブジェクトだと分かります。

inspect.tsx
const element = <a href="/about">About</a>;

console.log(element.type);  // 'a'
console.log(element.props); // { href: '/about', children: 'About' }
console.log(element.key);   // null

ReactElement の便利な点は、第1型引数で props の型を指定できることです。「このコンポーネントの要素だけを受け取りたい」という制約を型で表現できます。

TabList.tsx
import { Children, type ReactElement, type ReactNode } from 'react';

type TabProps = { label: string; children: ReactNode };

export function Tab({ children }: TabProps) {
  return <div role="tabpanel">{children}</div>;
}

type TabListProps = {
  // Tab の要素、またはその配列だけを受け取る
  children: ReactElement<TabProps> | ReactElement<TabProps>[];
};

export function TabList({ children }: TabListProps) {
  return (
    <div>
      {/* label を型安全に読み出せる */}
      {Children.map(children, (child) => (
        <button type="button">{child.props.label}</button>
      ))}
    </div>
  );
}

このように child.props.label を参照する必要があるときは、ReactNode ではなく ReactElement<TabProps> を要求する意味があります。逆に言えば、要素の中身に触らないなら ReactElement を要求する理由はありません

なお、React 19 向けの型定義では第1型引数 P のデフォルトが any から unknown に変更されました。ReactElement と型引数なしで書いて element.props.foo にアクセスしていたコードは、React 19 の型に上げると unknown 由来のエラーになることがあります。その場合は ReactElement<TabProps> のように props の型を明示してください。

JSX.Element は ReactElement の別名でしかない

JSX.Element は React 固有の型ではなく、TypeScript の JSX サポートの仕組みに由来します。TypeScript は JSX 式の型を決めるとき、JSX 名前空間の Element インターフェースを参照します。React はそこに自分の型を割り当てているだけで、@types/react の定義は実質1行です。

@types/react(抜粋)
namespace JSX {
  interface Element extends React.ReactElement<any, any> {}
  // IntrinsicElements(div などの組み込みタグ)などもここで定義される
}

つまり JSX.ElementReactElement<any, any> です。props が any なので型引数で絞り込むことはできず、ReactElement より情報量が少ない型だと考えて構いません。JSX 式を書いたときにエディタのツールチップに JSX.Element と出るのは、TypeScript がこの型を割り当てているからです。

もう一つ注意点があります。@types/react 18 まではこの JSX 名前空間がグローバルに宣言されていたため、import なしで JSX.Element と書けました。React 19 向けの型定義ではグローバル宣言が廃止され、React.JSX に置かれるようになりました。React 19 の型で使うなら次のように書きます。

Badge.tsx
// React 19 の型定義では JSX 名前空間を react から取り込む
import type { JSX } from 'react';

export function Badge(): JSX.Element {
  return <span className="badge">NEW</span>;
}

バージョンによって書き方が変わる型をわざわざ選ぶ理由は薄いので、戻り値の型を明示したいなら ReactElement を使うか、そもそも書かずに推論に任せるのが無難です。

children を ReactElement にすると渡せなくなるもの

「子要素はコンポーネントを1つ入れる想定だから」と children: ReactElementchildren: JSX.Element と書くと、使う側で困る場面が一気に増えます。どんなコードがエラーになるのか具体的に見てみます。

Panel.tsx(狭すぎる例)
import type { ReactElement } from 'react';

type PanelProps = { children: ReactElement };

export function Panel({ children }: PanelProps) {
  return <section className="panel">{children}</section>;
}
App.tsx
// OK: 要素1個ならそのまま通る
<Panel><p>本文</p></Panel>

// エラー: 文字列は ReactElement ではない
<Panel>テキストだけ渡したい</Panel>

// エラー: 要素が2個以上だと children が配列になる
<Panel>
  <h3>見出し</h3>
  <p>本文</p>
</Panel>

// エラー: map の結果は ReactElement[] になる
<Panel>{items.map((item) => <Row key={item.id} {...item} />)}</Panel>

// エラー: 条件が false のとき boolean になりうる
<Panel>{isOpen && <Body />}</Panel>

// エラー: 数値も ReactElement ではない
<Panel>{count}</Panel>

いずれも Type 'string' is not assignable to type 'ReactElement' のようなメッセージになります。これらは React として何も間違っていない書き方ばかりで、型が実態より狭いことが原因です。呼び出し側は <Panel><>{...}</></Panel> のようにフラグメントで包む回避策を取ることになりますが、本来必要のない手間です。children: ReactNode にしておけば全部そのまま通ります。

要素そのものを加工したい場合に限って ReactElement を要求し、そのときも配列を許す(ReactElement | ReactElement[])か、React.Children のユーティリティで正規化するのが現実的です。

PropsWithChildren と React.FC での書き方

children: ReactNode を毎回書くのが冗長に感じるときは、React が用意している PropsWithChildren<T> が使えます。定義は次の1行です。

@types/react(抜粋)
type PropsWithChildren<P = unknown> = P & { children?: ReactNode | undefined };

渡した props の型に、オプショナルな children を交差型で足すだけのユーティリティです。

Layout.tsx
import type { PropsWithChildren } from 'react';

type LayoutProps = PropsWithChildren<{ title: string }>;
// = { title: string } & { children?: ReactNode | undefined }

export function Layout({ title, children }: LayoutProps) {
  return (
    <main>
      <h1>{title}</h1>
      {children}
    </main>
  );
}

ここで押さえておきたいのが、@types/react 18 以降、React.FCchildren が暗黙で含まれなくなったことです。React 17 までの型定義では FCFunctionComponent)の props に children?: ReactNode が自動で足されていたため、const A: React.FC = ({ children }) => ... と書けました。この暗黙の追加は「children を受け取らないコンポーネントでも children を渡せてしまう」という問題があったため削除され、現在は自分で宣言する必要があります。

Section.tsx
import type { FC, PropsWithChildren, ReactNode } from 'react';

// React 17 までの書き方。18 以降の型では children がエラーになる
// const NG: FC = ({ children }) => <div>{children}</div>;

// 1. props に children を明示する
const Section: FC<{ children: ReactNode }> = ({ children }) => {
  return <section>{children}</section>;
};

// 2. PropsWithChildren を使う
const Article: FC<PropsWithChildren<{ title: string }>> = ({ title, children }) => {
  return (
    <article>
      <h2>{title}</h2>
      {children}
    </article>
  );
};

export { Section, Article };

そもそも React.FC を使わず、通常の関数宣言で props に型注釈を付ける書き方が現在の主流です。関数宣言なら戻り値は推論されますし、ジェネリックなコンポーネントも自然に書けます。

戻り値の型を書くときの選び方

コンポーネントの戻り値に型を書くかどうかは好みですが、書くなら ReactElement が扱いやすい選択です。ただし ReactElement は要素1個を表す型なので、null を返す可能性があるなら ReactElement | null と書く必要があります。

Notice.tsx
import type { ReactElement } from 'react';

type NoticeProps = { message?: string };

// 何も表示しないケースがあるので null を許す
export function Notice({ message }: NoticeProps): ReactElement | null {
  if (!message) {
    return null;
  }
  return <p role="status">{message}</p>;
}

文字列や配列をそのまま返すコンポーネントを書く場合は ReactNode を戻り値の型にします。この点は React.FC の定義もバージョンで変化しており、@types/react 18 では FunctionComponent の戻り値が ReactElement | null と定義されていたため、文字列を返す関数を FC 型の変数に代入すると型エラーになりました。React 19 向けの型定義では戻り値が ReactNode に緩和され、この制約はなくなっています。

迷うなら戻り値の型注釈を省くのが一番簡単です。JSX を返していれば TypeScript が自動的に JSX.Element(=ReactElement<any, any>)と推論してくれるので、注釈がなくても型安全性は変わりません。

ReactNode を props の一部として受け取る

ReactNode の出番は children だけではありません。「見出し部分」「アイコン」「操作ボタン」のように、複数の差し込み口を持つコンポーネントを作るときは、名前付きの props に ReactNode を使います。children は1つしか持てないため、この設計は実務でよく登場します。

Dialog.tsx
import type { ReactNode } from 'react';

type DialogProps = {
  // 文字列でも要素でも受け取れる
  header: ReactNode;
  icon?: ReactNode;
  footer?: ReactNode;
  children: ReactNode;
};

export function Dialog({ header, icon, footer, children }: DialogProps) {
  return (
    <div className="dialog" role="dialog">
      <div className="dialog__header">
        {icon}
        {header}
      </div>
      <div className="dialog__body">{children}</div>
      {footer && <div className="dialog__footer">{footer}</div>}
    </div>
  );
}
App.tsx
<Dialog
  header="削除の確認"
  icon={<WarningIcon />}
  footer={
    <>
      <button type="button">キャンセル</button>
      <button type="button">削除する</button>
    </>
  }
>
  <p>この操作は取り消せません。</p>
</Dialog>

header に文字列も要素も渡せるのは、ReactNode が両方を含んでいるからです。もしここを ReactElement にしていたら、header="削除の確認" という一番よく使う書き方ができなくなります。

渡された値が要素かどうか判定する

ReactNode で受け取った値に対して「要素のときだけ props を加工したい」という場面では、isValidElement() を使います。これは型ガード(引数が特定の型かを型レベルで絞り込む関数)として定義されているため、if の中では ReactElement として扱えるようになります。

IconSlot.tsx
import { cloneElement, isValidElement, type ReactNode } from 'react';

type IconSlotProps = { icon: ReactNode };

export function IconSlot({ icon }: IconSlotProps) {
  // 要素のときだけ className を追加する
  if (isValidElement<{ className?: string }>(icon)) {
    return cloneElement(icon, { className: 'icon icon--sm' });
  }

  // 文字列や数値などはそのまま出す
  return <span className="icon">{icon}</span>;
}

isValidElement() は React が内部で付けているマーカーを見て判定するので、単なるオブジェクトや文字列に対しては false を返します。型引数に props の形を渡しておくと、絞り込んだ後の icon.props に型が付いて安全に扱えます。

ComponentType・ElementType は「要素」ではなく「部品」の型

混同しやすいもう一組が ComponentTypeElementType です。これらはすでに作られた要素ではなく、要素を作るための部品そのものを表します。<Icon /> が要素なら、Icon が部品です。

表しているもの渡す値の例
ReactElement描画済みの要素オブジェクト<Icon />
ComponentType<P>関数コンポーネントまたはクラスコンポーネントIcon
ElementType<P>ComponentType に加えて 'div' などの組み込みタグ名も含むIcon'div''a'

部品を受け取る形にすると、コンポーネント側で props を決めてから描画できます。汎用ボタンのタグを切り替える as props や、アイコンだけを差し替える設計でよく使われます。

MenuItem.tsx
import type { ComponentType, ElementType } from 'react';

type MenuItemProps = {
  label: string;
  // 「アイコンの部品」を受け取り、size は呼び出し側で決めさせない
  Icon: ComponentType<{ size: number }>;
  // ラップするタグを差し替えられるようにする
  as?: ElementType;
};

export function MenuItem({ label, Icon, as: Tag = 'li' }: MenuItemProps) {
  return (
    <Tag className="menu-item">
      <Icon size={16} />
      {label}
    </Tag>
  );
}

// 使う側は要素ではなく関数そのものを渡す
// <MenuItem label="設定" Icon={GearIcon} as="div" />

受け取った値をそのまま置くだけなら ReactNode、コンポーネント側で props を渡して描画したいなら ComponentTypeElementType、という切り分けで考えると迷いません。

まとめ

ReactNodeReactElementJSX.Element は並列の選択肢ではなく、ReactNode が最も広く、その中に ReactElement が含まれ、JSX.ElementReactElement<any, any> の別名という関係にあります。ReactNode は文字列・数値・boolean・nullundefined・配列・要素まで含むため、childrenheadericon のように JSX を差し込む props はこれを使えば呼び出し側を制限しません。ReactElementtypepropskey を持つオブジェクトの型で、child.props を読むなど要素の中身に触るときや、戻り値の型を明示するときに使います。children をうっかり ReactElement にすると、文字列・複数要素・map の結果・条件付きレンダーがすべて型エラーになるので避けてください。PropsWithChildren<T> はオプショナルな children を足すだけのユーティリティで、@types/react 18 以降は React.FCchildren が暗黙で含まれないため、使う場合は自分で宣言します。値が要素かどうかは isValidElement() で判定でき、部品そのものを受け取りたいときは ComponentTypeElementType を選びます。

参考ページ