こんにちは。プロダクトの規模が膨らみ、日々の型エラーやリファクタリングに頭を悩ませている中級エンジニアの皆さん、お疲れ様です。
今回は、React 18以降のTypeScript環境で多くの開発者を「おっ?」と躓かせた、`React.PropsWithChildren` の仕様変更と、それに伴う型定義のベストプラクティスについてガッツリ解説していきます。
実務でコードを書いていると、「とりあえず `React.FC` 使っておけばいいか」「`PropsWithChildren` を挟んでおけばchildrenの型エラーは消えるだろ」と安直に逃げたくなる瞬間、ありますよね。しかし、その場しのぎの型定義は、のちのちコンポーネントの拡張性を縛り付け、コードベースを汚す原因になります。
React 18で何が変わり、私たちはどうコードを書くべきなのか。シニアの視点から、裏側の仕組みも含めて叩き込みます。
—
React 18で何が起きたのか? `PropsWithChildren` の暗黙的消滅
まず大前提として、React 18のリリース(正確には `@types/react` v18の導入)に伴い、Reactコミュニティに大きな激震が走りました。それは、`React.FC`(および `React.FunctionComponent`)の型定義から、`children` プロパティがデフォルトで除外されたという点です。
それ以前のReact 17までは、`React.FC` を使ってコンポーネントを定義しさえすれば、自分が書いたPropsのインターフェースに `children` を定義していなくても、勝手に `children` が生えてきていました。
// 【React 17以前の古き良き(しかし危うかった)世界】
type MyButtonProps = {
label: string;
};
// childrenを書いていないのに、なぜかJSXの子要素を渡せちゃう
const MyButton: React.FC
return ;
};
この仕様、一見すると便利ですが、実務では「本当は子要素を受け取るべきではないレイアウトコンポーネントに、意図せずchildrenが渡せてしまう」というバグの温床になっていました。コンポーネントの責務が曖昧になる原因だったのです。
そこでReactチームは、型安全性を厳格にするために、`React.FC` から `children` を剥ぎ取りました。「子要素を受け取りたいなら、明示的に型を定義しなさい」という、極めて真っ当な方向への舵切りです。
そこで登場するのが、今回の主役である `React.PropsWithChildren` です。
—
`React.PropsWithChildren` の現在地と、その限界
`React.PropsWithChildren
` は、渡された独自のProps(`P`)に、オプショナルな `children` プロパティをマージしてくれる便利なジェネリック型ユーティリティです。
例えば、以下のように使います。
import React from ‘react’;
type CardProps = React.PropsWithChildren<{ title: string; }>;
export const Card: React.FC
return (
{title}
);
};
これ自体は非常にスマートで、今でもよく使われます。しかし、実務の現場で大規模なアプリケーションを構築していくと、この `PropsWithChildren` には「ちょっとしたモヤモヤ」があることに気づきます。
それは何か? 「childrenが必ずオプショナル(`ReactNode | undefined`)になってしまう」という点です。
もしあなたが、「このコンポーネントは必ず子要素を受け取らなければならない(子要素がなければ成り立たない厳格なレイアウト)」という設計にしたい場合でも、`PropsWithChildren` を使うと `children` がオプショナル扱いになり、呼び出し側で子要素抜けのミス(`
—
現場がたどり着いた結論:明示的な `ReactNode` の型定義
こうした背景から、現在の最前線で戦うシニアエンジニアたちの間では、`React.PropsWithChildren` に頼るのではなく、明示的に `children?: React.ReactNode`(または必須なら `children: React.ReactNode`)を書くスタイルが強く推奨されています。
ブラウザのレンダリングやReactの内部処理(Reconciler)の観点から見ても、`children` の実体はReact要素、文字列、数値、ポータル、配列など、Reactが描画可能なあらゆるノード(`ReactNode`)の集合体に他なりません。型定義をボヤけさせず、最初から明確にしておく方が、TypeScriptのインテリセンス(入力補完)も働きやすく、コードの意図がドキュメントなしで一目で伝わります。
それでは、実務でそのまま使える、洗練されたコンポーネントのサンプルコードを見てみましょう。
実践:明示的な型定義を採用した堅牢なコンポーネント
import React from ‘react’;
/
- 1. 独自のProps定義に、childrenを明示的に含める
- チームメンバーへの指針:
- – オプショナルにする場合は `children?: React.ReactNode`
- – 必須にする場合は `children: React.ReactNode` (ラッパー系などで子要素が必須の場合)
/
type SectionContainerProps = {
heading: string;
className?: string;
children: React.ReactNode; // 明示的にReactNodeを指定!
};
/
- 2. あえて `React.FC` を使わず、通常の関数宣言(Arrow Function)にするスタイルも人気です。
- 戻り値の推論がクリアになり、余計な `children` の混入を防げます。
/
export const SectionContainer = ({
heading,
className = ”,
children,
}: SectionContainerProps) => {
return (
{heading}
{/
ブラウザのDOMツリーにマウントされる際、
Reactはこの `children` 内の仮想DOMノードを解釈し、
差分検出(Reconciliation)を行って効率的に実際のDOMを更新します。
/}
);
};
この書き方の何が素晴らしいかと言うと、「このコンポーネントには何が必須で、何がオプショナルなのか」が型定義を見ただけで1秒で理解できる点です。マジックのようなユーティリティ型に頼りすぎないことで、コードの可読性と保守性が劇的に向上します。
—
まとめ:チーフアーキテクトからのアドバイス
React 18以降の型定義の変化は、私たちにより厳格で意図の伝わりやすいコードを書くことを求めています。
1. `React.FC` のデフォルトの `children` はもう消えたという事実をチーム全体で共通認識にする。
2. 便利な `React.PropsWithChildren` は、手軽ではあるが「すべてがオプショナルになる」というトレードオフを理解して使う。
3. 迷ったら、明示的に `children: React.ReactNode`(または `?` 付き)を型定義に直書きする。これが最もバグを生まず、レビューもスムーズに進む現代のベストプラクティスです。
日々のコーディングで「なぜこの型にしているのか」を少しだけ意識するだけで、あなたの書くコードは確実にワンランク上の品質に到達します。さあ、明日からのコードレビューで、後輩たちにドヤ顔でこの知識をシェアしてあげてください!

コメント