対象読者: 現在React 18以前を使っていて、React 19にそのうち上げたいと思っている方。React 19の型定義の背景を知りたい方
みなさんこんにちは。筆者は最近気づいたのですが、React 18から19に上げるときに問題になる(型エラーになる)パターンがあります。React 19アップグレードガイドにもいくつか型の変更が載っていますが、それ以外のパターンです。
考えてみれば確かにそうなのですが、これに関する情報をあまり見かけたことがないのでメモ書き程度に記事にしておきます。
FCとReactNodeを返す関数
問題は、FC型にあります。FC型、つまりFunctionComponent型は、React 18ではだいたい以下のような定義でした1。
type FunctionComponent<P> = (props: P) => ReactNode;
一方で、React 19ではこのような定義になります。
type FunctionComponent<P> = (props: P) => ReactNode | Promise<ReactNode>;
また、「ReactNodeを返す関数」というのは、こんな感じで定義されたものを指します。
const foo: (props: { ... }): ReactNode => {
return <div>...</div>;
};
function bar(props: { ... }): ReactNode {
return <div>...</div>;
}
お察しのとおり、React 18では「FC」と「ReactNodeを返す関数」は同等のものでした。しかし、React 19では両者に差分があり、FCはPromise<ReactNode>を返すことが許可されています。
何が問題なのか
この状況では、「ReactNodeを返す関数」はFCの部分型になります。しかし、その逆は成り立ちません。
つまり、「ReactNodeを返す関数」が求められているところにFCを与えると型エラーになるのです。これが筆者が今回気づいた問題です。
具体例
具体例として、こんなコードを考えます。いわゆるrender propsパターンのコードです。これは、現代的な見方では、compositionパターンをさらに汎用化して引数を与えられるようにしたものと考えられます。
interface ErrorMessageProps {
renderMessage: (props: { error: string }) => ReactNode;
}
const ErrorMessage: FC<ErrorMessageProps> = ({ renderMessage }) => {
const error = "エラーが発生しました";
return <div>
{renderMessage({ error })}
</div>;
};
// 使い方
const renderInP = ({ error }: { error: string }) =>
<p className="error-message">{error}</p>;
<ErrorMessage renderMessage={renderInP} />
この例には特に問題はありません。
問題があるのは、使い方を次のようにしてしまった場合です。
interface ErrorMessageProps {
renderMessage: (props: { error: string }) => ReactNode;
}
const ErrorMessage: FC<ErrorMessageProps> = ({ renderMessage }) => {
const error = "エラーが発生しました";
return <div>
{renderMessage({ error })}
</div>;
};
// 使い方
const renderInP: FC<{ error: string }> = ({ error }) =>
<p className="error-message">{error}</p>;
<ErrorMessage renderMessage={renderInP} />
使い方で、renderInPの型を「ReactNodeを返す関数」ではなくFCにしました。
React 18ではこれで問題ありませんでしたが、React 19では <ErrorMessage renderMessage={renderInP} /> の箇所で以下の型エラーが発生します。
Type 'FC<{ error: string; }>' is not assignable to type '(props: { error: string; }) => ReactNode'.
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 'ReactElement<unknown, string | JSXElementConstructor<any>> | Iterable<ReactNode> | ReactPortal | Promise<AwaitedReactNode>'.
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'.
色々と書いてありますが、これはFCはReactNodeだけでなくPromise<ReactNode> を返す可能性があるのに、propsの型定義はPromise<ReactNode>を返すことを許していないことに起因します。
誰が悪いのか
悪いと書きましたが、誰かを責めたいわけではなく、技術的な原因がどこにあるのかということです。
二重Promise型
実は、Promise<ReactNode>という型は、定義を辿ると Promise<Promise<AwaitedReactNode>> という二重Promiseの型を含んでいます。
JavaScriptでは、ランタイムでこのような「二重のPromise」が発生することはありません。しかし、型ではこのような型を作ることができます。
@types/reactのコントリビューターからはTypeScript側が対処すべき問題であると考えられています。
つまり、TypeScriptがこのような二重Promiseを自動的に解消して Promise<AwaitedReactNode> と同等とみなしてくれるのであれば、この記事の問題は起こらないということです。
Reactの型定義
上記のとおり、実はReactNodeはすでにPromiseを含んでいます。
ReactNodeの型の中にPromiseが現れているのは、React 19でサーバーコンポーネントのサポートが追加されたからです。サーバーコンポーネントでは、関数コンポーネントをasync関数にすることができます。これに対応するためにこのような型定義になっています。
// @types/react 19.2.10のソースコードから引用
type ReactNode =
| ReactElement
| string
| number
| bigint
| Iterable<ReactNode>
| ReactPortal
| boolean
| null
| undefined
| DO_NOT_USE_OR_YOU_WILL_BE_FIRED_EXPERIMENTAL_REACT_NODES[
keyof DO_NOT_USE_OR_YOU_WILL_BE_FIRED_EXPERIMENTAL_REACT_NODES
]
| Promise<AwaitedReactNode>;
AwaitedReactNodeというのはReactNodeからPromiseを抜いた版です。
つまり、Promise<ReactNode>としなくても、ReactNode自体がすでにPromiseを含み、しかも二重Promiseを回避する定義になっています。
では、なぜPromise<ReactNode>という定義が追加されたのでしょうか。その経緯は以下のプルリクエスト・ディスカッションで知ることができます。
// 2つ目のリンク先から引用
function Page() {
// 'PageContent' cannot be used as a JSX component.
// Its type '() => Promise<ReactNode>' is not a valid JSX element type.
// Type '() => Promise<ReactNode>' is not assignable to type '(props: any) => 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'.ts(2786)
return <PageContent />
}
async function PageContent(): Promise<ReactNode> {
return <div>Check</div>
}
つまり、async関数の返り値を明記するとき、従来の関数コンポーネントがReactNodeを返していたことから類推して Promise<ReactNode> としたくなるが、それだとエラーになってしまう問題があったのです。
これに対して Promise<ReactNode> をFCの返り値型に加えてあげることでエラーにならないようにしたのがリンク先の修正です。
理想的には、ReactNodeがすでにPromiseを含んでいるのですから、こう書けてもいいはずです。
// こう書きたいが……
async function PageContent(): ReactNode {
return <div>Check</div>
}
しかし、これはTypeScriptが許してくれません。async関数の返り値の型アノテーションは、TypeScriptの制約でPromiseと明記される必要があります。代わりに許されるのは以下のような書き方です。
// これらはOK
async function PageContent(): Promise<ReactElement> {
return <div>Check</div>
}
async function PageContent(): Promise<Awaited<ReactNode>> {
return <div>Check</div>
}
しかし、これらは一般のReact利用者に推奨するには自然さに欠けると思われたのでしょう。その結果として、FunctionComponentの返り値にPromise<ReactNode>が追加されたのです。
つまり、「TypeScriptの言語使用上の制約」と「Promise<ReactNode>と書ける分かりやすさ」を両立した結果です。
しかし……
実は、async関数を「ReactNodeを返す関数」に代入すること自体は可能です。
// これはOK
type MyFC<P = {}> = (props: P) => ReactNode;
const PageContent: MyFC = async () => {
return <div>Check</div>
}
つまり、const Component: FC = ... の書き方に寄せていれば、FCの返り値にPromiseを追加しなくても問題ないのです。
関数宣言よりもFC派の筆者としてはそれ見たことかと思わなくもありませんが、async functionで書きたい需要にも配慮してこうなったのでしょう。
ちなみに、async functionの返り値をアノテーションせずに、推論に任せるのも問題ありません。Promise<ReactNode>ではなくより具体的なPromise<ReactElement>で推論されるからです。今回配慮の対象となっているのは、async functionでコンポーネントを定義する上に、返り値の型も明記する必要がある人たちなのです。
React 18時点で注意すべきこと
話を本題に戻します。React 18から19へのアップグレードの話でしたね。
現状を踏まえると、将来のReact 19への移行をスムーズにするためにReact 18時点で注意すべきことは、FCと「ReactNodeを返す関数」を混同しないことです。型エラーが発生する例を再掲します。
interface ErrorMessageProps {
renderMessage: (props: { error: string }) => ReactNode;
}
const ErrorMessage: FC<ErrorMessageProps> = ({ renderMessage }) => {
const error = "エラーが発生しました";
return <div>
{renderMessage({ error })}
</div>;
};
// 使い方
const renderInP: FC<{ error: string }> = ({ error }) =>
<p className="error-message">{error}</p>;
// ここで型エラーが発生!
<ErrorMessage renderMessage={renderInP} />
この例で(React 19で)型エラーが出てしまうのは、FCと「ReactNodeを返す関数」を混同したからです。
もっと言えば、この場合、renderInP関数にFCという型注釈を与えたのは、意味的に考えれば間違いです。
なぜなら、関数コンポーネントというのは<Component />のようにJSXを通じて使用することが意図されたものであり、renderInP({ ... }) のように呼び出すべきではないからです。
ReactNodeを返す関数がFCの部分型であることも踏まえると、以下の表のように整理できます。
がやってはいけないことです。
| FC | ReactNodeを返す関数 | |
|---|---|---|
| 関数コンポーネント | ||
| 非関数コンポーネント |
上の例では、 renderInP という非関数コンポーネント(ただの関数として使われるもの)に対して、 FC という型注釈を与えてしまっているので
です。
まとめ
「コンポーネントではない、ただReactNodeを返すだけの関数」に FC という型注釈を与えることは、React 18では問題ありませんが、React 19では型エラーになってしまいます。
できれば、コンポーネントとコンポーネント以外をちゃんと使い分けて、適切な型定義を与えるようにしましょう。
-
実際は
displayNameやpropsTypeやレガシーContext APIなどがあるのでもう少し複雑な定義ですが、この記事の説明のためにはこの定義で十分です。 ↩