このチュートリアルでは、Reactアプリケーションの構築にTypeScriptを使用する方法を、Create-React-App (CRA)を利用して簡単に始められる手順とともに解説します。
また、TypeScriptを使用した独自のReact関数コンポーネントでのPropsの定義方法も紹介します。
前提として、あなたがNode.jsを既にインストールしていることを仮定しています。
また、このチュートリアルはTypeScriptの基本的な知識があることも前提としています。
ReactとTypeScriptの組み合わせについて学ぶことで、より堅牢なアプリケーションを開発するためのツールとしての利用価値を高めることができます。
この記事を参考にして、ReactとTypeScriptの基本をマスターしましょう。
TypeScriptとReactの利点
Reactアプリケーションを構築するための新しい方法として、TypeScriptが広く採用されています。
TypeScriptはJavaScriptのスーパーセットであり、型付き構文をサポートしています。
コンパイルすることで、ブラウザで実行できるプレーンなJavaScriptに変換されます。
TypeScriptは読みやすさと型チェックの恩恵により、フロントエンドWeb開発でますます人気が高まっています。
静的型システムは問題を早期に発見するために役立ちます。
例えば、必要なPropsを提供していない場合、TypeScriptコンパイラはこれを検出して報告します。
VS Codeを使用する場合は、赤い波線でそれを指摘してくれます。
TypeScriptとReactは完璧に相性が良く、両方を組み合わせることでアプリケーション開発の生産性を大幅に向上させることができます。
クリーンなコードでバグを減らし、よりシンプルなドキュメントを作成することができます。
開発者がコードを保守しやすくなり、コンポーネント間で渡されるプロパティとデータ型をチェックすることでバグを防止し、実装の意図を明確にすることができます。
大規模なReactプロジェクトにとって、TypeScriptは価値のあるオプションであることがわかります。
静的型チェックは、プロジェクトの拡大に伴い、開発者が迅速かつ正確に修正できるようにしてくれます。
TypeScriptを利用することで、Reactアプリケーションの品質と保守性を向上させることができます。
CRAのインストール
Create-React-App(CRA)は、コマンドライン(CLI)ツールで、WebpackやBabelなどに気を使うことなく、デフォルトとゼロの構成で新しいReactアプリを簡単に作成し、開発を開始できます。
下記のコマンドを使用して、グローバルにインストールすることができます。
npm install -g create-react-app
TypeScriptは次のステップで、npx
を使用してインストールが可能です。
プログラムを開発する際におすすめのエディタはVSCodeです。軽量で高速であり、必要なすべての機能を提供してくれます。
ただし、SublimeText
、WebStorm
、Atom
などの他のエディタを使用したい場合は、自由に選択することができます。
この記事ではエディタの選択は重要ではありません。
TypeScriptでReactアプリを初期化
TypeScriptを使用したReactアプリケーションを作成するためには、CRAコマンドに--template typescript
オプションを追加します。
例えば、下記のコマンドを実行すると、TypeScriptを使用した新しいReactアプリケーションが作成されます。
npx create-react-app my-app --template typescript
npx
コマンドは、CLIコマンドやレジストリにホストされている実行ファイルを実行するために使用されます。
これにより、グローバルインストールは必要ありません。
プロジェクトが作成されると、プロジェクトディレクトリに移動して任意のコードエディタで開くことができます。
プロジェクトファイル構成は、下記のようになります。
my-app/ ├─ node_modules/ ├─ public/ | — favicon.ico | — index.html | — logo192.png | — logo512.png | — manifest.json | — robots.txt ├─ src/ | — App.css | — App.test.tsx | — App.tsx | — index.css | — index.tsx | — logo.svg | — react-app-env.d.ts | — reportWebVitals.ts | — setupTests.ts | — registerServiceWorker.ts ├─ .gitignore ├─ package.lock.json ├─ package.json ├─ README.md └─ tsconfig.json
tsconfig.json
tsconfig.json
ファイルは、TypeScriptプロジェクトのコンパイラ設定とファイルを示す設定ファイルであり、JavaScriptプロジェクトのpackage.json
と同様の役割を果たします。
{ "compilerOptions": { "target": "es5", "lib": [ "dom", "dom.iterable", "esnext" ], "allowJs": true, "skipLibCheck": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, "strict": true, "forceConsistentCasingInFileNames": true, "noFallthroughCasesInSwitch": true, "module": "esnext", "moduleResolution": "node", "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "react-jsx" }, "include": [ "src" ] }
CRAによって生成されたtsconfig.json
は、TypeScriptのコンパイラが使用する設定を含んでいます。
例えば、"target": "es5"
は、コードをどのECMAScriptバージョンにコンパイルするかを指定するオプションです。
この場合、古いブラウザとの互換性を高めるために、ECMAScript 5にコンパイルされます。
また、"lib": ["dom", "dom.iterable", "esnext"]
は、コードがどのライブラリを使用するかを示すオプションです。
ここでは、DOMとDOMのイテレータ(配列)を使用することを指定しています。
また、"esnext"
を含めることで、最新のJavaScript機能を使用できるようになります。
さらに、"jsx": "react-jsx"
は、JSXを使用してReactコンポーネントを記述するための設定です。
このオプションは、Reactが提供するReact.createElement()
を呼び出す代わりに、JSXシンタックスを使用してコンポーネントを記述することを可能にします。
"include": ["src"]
は、TypeScriptコンパイラに含めるファイルを指定するオプションです。
ここでは、src
フォルダー内のすべてのファイルを含めるように指定されています。
これらのオプションは、TypeScriptコンパイル時に重要な役割を果たします。
設定ファイルを変更することで、TypeScriptプロジェクトのコンパイル設定をカスタマイズすることができます。
以上が、CRAでTypeScriptを使用する場合の基本的な設定とファイル構成についての解説です。
JSXには.tsxファイルを使用
TypeScriptを使用してReactを開発する場合、通常のReactアプリの作成方法にいくつかの変更が必要です。
.ts
拡張子ファイルに加えて、.tsx
という新しいファイル拡張子があります。
この拡張子は、JSX構文を含むReactコンポーネントファイルを作成する場合に使用します。
TypeScriptは、JSXを直接JavaScriptにコンパイルできるため、TypeScriptを使用することでReactアプリケーションをより型安全に、かつコードの品質を高めることができます。
TypeScriptを使用することで、パッケージをインストールする必要がなくなるため、開発プロセスがスピードアップすることもあります。
.tsx
ファイルを使用することによって、WebpackやES BuildなどのJavaScriptバンドラーは、.tsx
ファイルに対して特別なプラグインを実行することができ、Jestのようなテストランナーは.tsx
ファイルのテスト環境を実行することができます。
また、.tsx
ファイル拡張子のサポートにより、コードエディターが新しい言語機能をサポートすることができます。
.ts
ファイル拡張子では、JSX構文や要素の使用を必要としない関数、クラス、レデューサーなどを作成する場合に使用されます。
一方、.tsx
ファイル拡張子は、Reactコンポーネントを作成し、JSX要素および構文を使用する場合に使用されます。
PropsとStateにTypeScriptを使用する場合、学習コストがかかるかもしれませんが、一度理解するとReactのデフォルトの方法よりもはるかに強力であることがわかるでしょう。
TypeScriptコンポーネント作成
まず、Reactアプリケーションの初期ファイルであるCRAデフォルトのApp.tsxファイルを開きます。
このファイルには、Reactアプリケーションのルートコンポーネントが含まれています。
最初に表示されるロゴやCSS、およびimg
タグやa
タグは必要ないので、それらを削除して、下記のように簡略化できます。
function App() { return ( <div className="App"> <header className="App-header"> </header> </div> ); } export default App;
それでは、このまま、関数コンポーネントを作成していきましょう。
まずは、TypeScriptを使用しない例として Propsという機能の流れを掴んでいきます。
下記の例では、Appコンポーネントが親として、Child
コンポーネントが子の関数コンポーネントとなります。
user
というオブジェクトと、Num
を作る関数、そして2つの変数の値を宣言しています。
親コンポーネントで、子コンポーネントに渡すPropsを設定し、それをChild
コンポーネントに渡しています。
// 親コンポーネント function App() { const user = { age: 30, name: "Taro" } const funcNum = num => { return num + 2 } const title = "Hello!!, React with TypeScript." const content = "propsを送ります" return ( <div> <Child title={title} content={content} user={user} num={funcNum} /> </div> ) }
子の関数コンポーネントは、最初の引数としてPropsをオブジェクトとして受け取ります。
親コンポーネントで設定された各属性は、子コンポーネントのPropsの名前となります。
一般的な方法では、Propsを分解して取り出すことができます。
下記の例では、Propsの各プロパティを分解して、それぞれを変数に割り当てています。
// 子コンポーネント export default function Child({ title, content, user, num }) { console.log(num(3)) // 5 return ( <div> <h2>{title}</h2> <p>{content}</p> <p>{user.age}</p> <p>{user.name}</p> </div> ) }
See the Pen React with Typescript interface 拡張 by dev.K | Webアプリ開発者 (@enjinia_f) on CodePen.
このように、受け取ったPropsオブジェクトから必要な値を分解して取り出すことで、コードの可読性が向上します。
また、Propsにはデフォルト値を設定することもできます。
これは、Propsが渡されなかった場合にその値が使用されるため、コンポーネントの使用時に必須の値であるかどうかを明示することができます。
export default function Child({ title = "Default Title", content = "Default Content", user = {}, num = () => {} }) { console.log(num(3)) // 5 return ( <div> <h2>{title}</h2> <p>{content}</p> <p>{user.age}</p> <p>{user.name}</p> </div> ) }
ここでは、Propsの値が渡されなかった場合に、title
には"Default Title"
、content
には"Default Content"
、user
には空のオブジェクト、num
には空の関数が使用されるように設定しています。
以上のように、Propsを適切に扱うことで、Reactコンポーネントの再利用性と保守性を高めることができます。
Propsの渡し方
ReactでTypeScriptを使用して関数コンポーネントを作成する際には、Propsの型チェックを行うことが推奨されます。
親コンポーネントは下記となります。
// 親コンポーネント function App() { return ( <div className="App"> <header className="App-header"> <MyMessage /> </header> </div> ); }
MyMessage
という子の関数コンポーネントを作成します。
// 子コンポーネント const MyMessage = ( ) => { return <div><h2>Welcome to the {message} of React and TypeScript.</h2></div>; }
全てのPropsに対して、オブジェクトとして型を定義をしなければいけません。
Propsの型を定義する方法はいくつかあります。
まず、インラインで型を定義する方法があります。
下記のように、Propsをオブジェクトとして定義し、関数の引数として渡します。
const MyMessage = ({ message }: { message: string }) => { return <div> <h2> Welcome to the {message} of React and TypeScript.</h2> </div>; }
See the Pen React with Typescript interface by dev.K | Webアプリ開発者 (@enjinia_f) on CodePen.
次に、Propsオブジェクトで型を定義する方法があります。
下記のように、Propsオブジェクトを引数として渡します。
const MyMessage = (props: { message: string }) => { return <div> <h2>Welcome to the {props.message} of React and TypeScript.</h2> </div>; }
See the Pen React with Typescript inlineで定義 by dev.K | Webアプリ開発者 (@enjinia_f) on CodePen.
また、interface
を使用して、Propsの型を定義する方法があります。
下記のように、Propsの型をinterface
として定義し、関数の引数として渡します。
interface MessageProps { message: string; }; const MyMessage = (props: MessageProps) => { return<div> <h2>Welcome to the {props.message} of React and TypeScript. </h2> </div>; }
子コンポーネントには、message
というプロパティが必要であり、その値は文字列である必要があります。
親コンポーネントからPropsを渡す場合、下記のようにmessage
プロパティに文字列の値を渡すことができます。
function App() { return ( <div className="App"> <header className="App-header"> <MyMessage message="world" /> </header> </div> ); } export default App;
See the Pen React with Typescript interface by dev.K | Webアプリ開発者 (@enjinia_f) on CodePen.
同様に、下記の例では、3つのPropsを持つコンポーネントに型注釈を付けています。
interface UserProps { name: string; age: number; country: string; }; function UserData({name, age, country}: UserProps) { return ( <div> <h2>{name}</h2> <h2>{age}</h2> <h2>{country}</h2> </div> ); } export default function App() { const obj = {...{name: 'Taro', age: 30, country: 'Japan'}}; return ( <div> <UserData {...obj} /> </div> ); }
See the Pen React with Typescript interface 複数 by dev.K | Webアプリ開発者 (@enjinia_f) on CodePen.
UserProps
は、name
、age
、country
の3つのプロパティを持つオブジェクトを表しています。
UserData
コンポーネントは、この3つのプロパティをPropsとして受け取り、それらを表示します。
親コンポーネントでは、UserDataコンポーネントにPropsとしてオブジェクトを渡す際に、スプレッド構文(...)を使用してオブジェクトのプロパティを展開しています。
Propsの型を正確に定義することで、コンポーネント間での値の受け渡しを安全に行うことができます。
また、TypeScriptを使用することで、開発時のデバッグが容易になり、バグを防ぐことができます。
型エイリアス(type alias)
型エイリアス(type alias)は、TypeScriptで型を再利用するために使用されます。
interface
を使うこともできますが、一貫性と明確性を保つために、型エイリアスを使用することが推奨されます。
Reactアプリケーションで、特定のコンポーネントが受け取るPropsを定義する必要がある場合、型エイリアスではなくinterface
を使用することが理想的です。
ただし、Propsの関係が複雑な場合は、型エイリアスを使用することがあります。
型エイリアスは、プリミティブ型だけでなく、オブジェクト型、ユニオン型、タプル型、および交差型を表すために使用できます。
型エイリアスは、type
キーワードを使って宣言します。
type Props = { title: string, score: number } const App = ({ title, score }: Props) => ( <h1>{title} = {score}</h1> )
また、型に名前を与えることで可読性が向上します。
type userName = string; type userId = string | number; type arr = number[];
変数と同様に、同じ型のエイリアスを複数回宣言することはできません。
また、TypeScriptがエイリアスを推論することはなく、明示的に型注釈しなければなりません。
以下は、型エイリアスを使った例です。
type PersonAge = number; type PersonName = string; type PersonMail = string; type Person = { age: PersonAge, name: PersonName, mail: PersonMail }; const personAge: PersonAge = 30 const personName: PersonName = "Taro" const personMail: PersonMail = "xxx.xxx@gmail.com" const person: Person = { age: personAge, name: personName, mail: personMail }; console.log(person);
コンソールの出力は下記で確認可能です。
https://codepen.io/enjinia_f/pen/YzawvzP/?editors=1011
上記の例では、型エイリアスを使用して、Person
オブジェクトのプロパティの型を明示的に指定しています。
これにより、コードの可読性が向上し、保守性が向上します。
宣言のマージと拡張
ReactとTypeScriptの組み合わせで、Propsを扱う際に非常に重要な機能があります。
それは、型の定義方法に関するものです。
TypeScriptでは、型エイリアスとinterface
の2つの型の定義方法があります。
型エイリアスは、既存の型に対して別名を付けるためのもので、下記のように定義することができます。
type MyString = string;
一方、interface
は、オブジェクトの形状を定義するためのもので、下記のように定義することができます。
interface MyObject { name: string; age: number; }
このように、型エイリアスとinterfaceはそれぞれ異なる目的を持っていますが、いくつかの点で異なる振る舞いをすることがあります。
まず、宣言のマージについて考えてみましょう。
宣言のマージとは、TypeScriptコンパイラが同じ名前を持つ2つ以上のinterfaceを1つにマージすることです。
例えば、下記のように2つのinterfaceを定義することができます。
interface User { name: string; } interface User { age: number; }
この場合、TypeScriptは自動的に両方のinterfaceをマージして、下記のように解釈します。
interface User { name: string; age: number; }
一方、型エイリアスでは宣言のマージは行われません。
同じ名前の型エイリアスを複数回定義すると、エラーが発生します。
type MyType = { name: string; } type MyType = { age: number; } // Error: Duplicate identifier 'MyType'
次に、型の拡張について考えてみましょう。
interfaceは、他のinterfaceを拡張することができます。
これにより、元のinterfaceに新しいプロパティを追加したり、既存のプロパティを上書きすることができます。
下記は、interfaceを拡張する例です。
interface Person { name: string; } interface Employee extends Person { salary: number; } const employee: Employee = { name: "Alice", salary: 1000 };
一方で、型エイリアスは拡張ができません。
同じ名前の型エイリアスを定義することはできますが、既存の型に対して新しいプロパティを追加することはできません。
type MyType = { name: string; } type MyType = { age: number; // Error: 'name' is already declared in the same scope
Children props
Reactにおいて、Propsを受け取ってレンダリングするChildren機能は、最も価値のある機能の1つです。
このChildren propsを使用することで、コンポーネントをまとめて構成することができます。
この概念は、再利用可能なコンポーネントを構築するために非常に重要です。
親コンポーネントが子コンポーネントに直接何らかのコンテンツを渡すためには、マークアップまたはアクションでカプセル化するだけでよく、子コンポーネントはprops.children変数を通じて、渡されたchildrenにアクセスすることができます。
ほとんどのJavaScriptのデータ型を渡すことができます。
数値、文字列、真偽値、要素の配列、あるいは他のコンポーネントも可能です。
しかし、型を明示的に定義することが望ましいです。
これにより、TypeScriptを使用して型チェックを行うことができます。
propsの型を定義する場合、いくつかのオプションがあります。
JSX.Element
Reactでは、Props.children
プロパティの多くはJSX要素で構成されており、これらの要素を型付けするためにJSX.Element
という特別な型を使用できます。
React要素は、コンポーネントやDOM要素を表すオブジェクトであり、JSX要素がJavaScriptに変換された結果として生成されます。
JSX.Element
は、React環境でグローバルに利用できる特別な型です。
コンポーネントがレンダリングされるとき、開始タグと終了タグの間に含まれるコンテンツを保持します。
例えば下記のように、Propsにtitle
とchildren
を定義した場合です。
type Props = { title: string, children: JSX.Element; }; const Component = ({ title, children }: Props) => ( <div> <h1>{title}</h1> {children} </div> ); const App = () => { return( <Component> <span>React children</span> </Component> ); };
JSX.Element
型は、子コンポーネントが受け取るReact要素の配列を表します。
子コンポーネントは、この配列内のReact要素にアクセスし、それらをレンダリングすることができます。
ただし、JSX.Element
は、children
に使用できる最も制限的な型であり、Propsが単一のReact要素である場合にのみ機能します。
他のプリミティブな値、例えば文字列などは、children
に許可されていません。
もし、コンポーネントの消費者がこれをオプションにしたい場合は、?
を付けた型注釈を使用することができます。
type Props = { title: string; children?: JSX.Element; };
ただし、JSX.Element
は複数の子要素を許可しません。複数の子要素を持つ場合、または文字列などのプリミティブなものを許可したい場合は、他のオプションを検討する必要があります。
JSX.Element [ ]
JSX.Element[]
は、複数のJSX要素を含む配列を表します。
この型は、Reactで複数の要素をレンダリングする際によく使用されます。
複数のJSX要素を許可しますが、文字列、数字などは許可しません。
下記では、JSX.Element[]の型は明示的に定義していない事に注意してください。
interface Fruit { name: string; color: string; } interface FruitListProps { fruits: Fruit[]; } function FruitList({ fruits }: FruitListProps) { return ( <ul> {fruits.map((fruit, index) => ( <li key={index}> <span>{fruit.name}</span> <span>{fruit.color}</span> </li> ))} </ul> ); }
上記の例では、FruitList
コンポーネントにfruits
プロパティが渡されます。
fruits
プロパティは、Fruit
型の配列であり、Fruit
型は、name
、color
の2つのプロパティを持ちます。
FruitList
コンポーネントでは、fruits
プロパティの各要素をレンダリングするために、mapメソッドを使用しています。
mapメソッドは、各要素に対して、key
プロパティを指定したli
要素を返します。
li
要素には、各フルーツのname
とcolor
が含まれています。
JSX.Element[]
はReactの型の一つで、Reactライブラリの中で定義されています。
FruitList
関数コンポーネントは、JSX.Element[]
の型を返すことが期待されており、JSX構文を使用してUIをレンダリングしています。
そのため、TypeScriptはこの関数がJSX.Element[]
の型を返すと推論し、型チェックを行います。
したがって、上記のコードではJSX.Element[]
の型を明示的に定義する必要はありません。
このように、JSX.Element[]
を使うことで、Reactで複数の要素を効率的にレンダリングすることができます。
ですが、もちろん明示的にJSX.Element[]
の型を定義する必要がある場合があります。
例えば、下記のような場合です。
・ 返される要素がJSX.Element[]
ではない場合
たとえば、Reactコンポーネントが単一の要素を返すように定義されている場合、型推論が期待通りに機能しない場合があります。
この場合、返される要素の型をJSX.Element[]
として明示的に定義する必要があります。
・ ネストされたコンポーネントの型が異なる場合
ネストされたコンポーネントの型がJSX.Element[]
と一致しない場合、明示的に型を定義する必要があります。
・ 外部ライブラリで定義されたコンポーネントを使用する場合
外部ライブラリで定義されたコンポーネントを使用する場合、そのコンポーネントの型がJSX.Element[]
と一致しない場合があります。
この場合、明示的に型を定義する必要があります。
以上のような場合、JSX.Element[]
の型を明示的に定義することで、TypeScriptはコードをより厳密にチェックできるようになります。
JSX.Element | JSX.Element []
一方、JSX.Element | JSX.Element[]
は、単一または複数のJSX要素を表します。
この型は、コンポーネントのプロパティで単一の要素または複数の要素の配列を受け取ることができる場合に使用されます。
ただし、文字列や数字などのプリミティブ型は許可されず、JSX要素のみが許可されます。
例えば、Message
コンポーネントのchildren
プロパティはJSX.Element | JSX.Element[]
型であり、単一のJSX要素やJSX要素の配列を受け取ることができます。
これを使って、Reactアプリケーションでメッセージを表示する場合、下記のように記述することができます。
interface MessageProps { children: JSX.Element | JSX.Element[]; notice: boolean; }; function Message({ children, notice }: MessageProps) { return ( <div> {notice ? 'お知らせ: ' : 'メッセージ: '} {children} </div> ); } function App() { return ( <div className="App"> <header className="App-header"> <Message notice={false}> <span>Hello World. </span> <span>こちらは、複数のJSX要素を許可します。</span> <i>アイコン</i> </Message> </header> </div> ); }
上記のコードでは、Message
コンポーネントを定義し、children
プロパティで受け取った要素を表示しています。
Appコンポーネントでは、Message
コンポーネントに複数のJSX要素を渡しています。
Message
コンポーネントは、受け取ったnotice
プロパティに応じてメッセージを表示するようになっています。
これにより、Reactアプリケーションで複数の要素を組み合わせたメッセージを表示することができます。
上記以外の、選択できる型はございます。
・ React.ReactChild
1つのReact要素、文字列、または数値を許可します。
import React, { ReactChild } from 'react'; interface GreetingProps { children: ReactChild; } function Greeting({ children }: GreetingProps) { return <div>Hello, {children}!</div>; }
上記の例では、Greeting
コンポーネントにchildren
プロパティが渡されます。
children
プロパティは、ReactChild
型であり、文字列、数値、React要素など、さまざまな種類の要素を受け取ることができます。
Greeting
コンポーネントでは、children
プロパティを含むdiv
要素を返します。
children
プロパティの値が、Hello,
という文字列の前に表示されます。
・ React.ReactChild [ ]
ReactChild[]
は、単一の要素や配列を含むことができるReact要素の配列を表します。
import React, { ReactChild } from 'react'; interface MessageProps { children: ReactChild[]; } function Message({ children }: MessageProps) { return ( <div> {children.map((child, index) => ( <span key={index}>{child} </span> ))} </div> ); }
上記の例では、 Message
コンポーネントに children
プロパティが渡されます。
children
プロパティは、 ReactChild
型の配列であり、文字列や数値、React要素など、複数の種類の要素を含むことができます。
Message
コンポーネントでは、 children
プロパティの各要素をレンダリングするために、mapメソッドを使用しています。
mapメソッドは、各要素に対して、 key
プロパティを指定した span
要素を返します。
この、 span
要素には、各子要素が含まれています。
上記のように、 ReactChild[]
を使うことで、Reactで複数の要素を効率的にレンダリングすることができます。
・ React.ReactChild | React.ReactChild [ ]
単一の要素または要素の配列を含むReact要素を表します。
import React, { ReactChild } from 'react'; interface MessageProps { children: ReactChild | ReactChild[]; } function Message({ children }: MessageProps) { return ( <div> {Array.isArray(children) ? children.map((child, index) => <span key={index}>{child} </span>) : children} </div> ); }
上記の例では、Message
コンポーネントにchildren
プロパティが渡されます。
children
プロパティは、ReactChild
型またはReactChild
型の配列であり、文字列や数値、React要素など、複数の種類の要素を含むことができます。
Message
コンポーネントでは、children
プロパティの型によって、単一の要素をレンダリングするか、配列をmapメソッドで反復処理してレンダリングするかを判断しています。
ReactのisArrary
関数を使用して、children
が配列であるかどうかを判定し、配列の場合はmapメソッドを使用して各要素をレンダリングします。
単一の要素の場合は、children
を直接レンダリングします。
上記のように、ReactChild | ReactChild[]
を使うことで、Reactで単一の要素または複数の要素をレンダリングすることができます。
・ React.ReactNode
複数の子、文字列、数値、フラグメント、ポータルを許可します。
import React from 'react'; interface MessageProps { message: React.ReactNode; } function Message({ message }: MessageProps) { return ( <div> <h1>Welcome to the world of React and TypeScript.</h1> {message} </div> ); } function App() { return ( <div className="App"> <header className="App-header"> <Message message={<h3>ReactNode</h3>} /> </header> </div> ); }
See the Pen React with Typescript Props React.ReactNode by dev.K | Webアプリ開発者 (@enjinia_f) on CodePen.
上記の例では、Message
コンポーネントにmessage
プロパティが渡されます。
message
プロパティは、React.ReactNode
型であり、文字列や数値、React要素など、複数の種類の要素を含むことができます。
Message
コンポーネントでは、message
プロパティを直接レンダリングしています。
上記のように、React.ReactNode
を使うことで、Reactでさまざまな種類の要素をレンダリングすることができます。
・ React.ReactNode | React.ReactNode[ ]
React.ReactNode
は、Reactコンポーネントでレンダリング可能な全てのオブジェクトを表す TypeScriptの型定義です。
これは、文字列、数値、Reactコンポーネント、null
、undefined
、またはこれらの型の配列のいずれかを表すことができます。
一方、React.ReactNode[]
は、ReactNode
の配列を表します。
import React from 'react'; interface ExampleComponentProps { text: React.ReactNode; list: React.ReactNode[]; } function ExampleComponent({ text, list }: ExampleComponentProps) { return ( <div> <p>{text}</p> <ul> {list.map((item, index) => ( <li key={index}>{item}</li> ))} </ul> </div> ); } function App() { return ( <ExampleComponent text="Hello, world!" list={[1, 2, 3, 4].map((num) => ( <span>{num}</span> ))} /> ); }
上記では、ExampleComponent
関数コンポーネントのプロパティには、 React.ReactNode
型のtext
プロパティと React.ReactNode[]
型のlist
プロパティが含まれています。
App 関数コンポーネントは、ExampleComponent
にtext
とlist
を渡します。
list
は、数字の配列を map() メソッドでループし、各要素を<span>
要素として含む配列に変換されます。
React children
のおさらいも含めて、他の例でも見ていきましょう。
type Props = { content: string, children: JSX.Element; }; const Child = ({ content, children }: Props) => ( <div> <h1>{content}</h1> {children} </div> );
オプションにしたい場合は、型注釈の前にクエスチョンマーク( ? )
を付けます。
type Props = { content: string, children?: JSX.Element; };
タグなしでPropsオブジェクトを作成できることをコンパイラに通知します。
JSX.Element
は、子要素が単一のReact要素であることが要求される場合はよいでしょう。
しかし、複数の子要素は許しません。
そこで、下記のように調整します。
type Props = { content: string, children?: JSX.Element | JSX.Element[]; };
ですが、JSX.Element
の欠点は文字列を許さないことです。
そこで、ユニオン型に文字列を追加することができます。
type Props = { content: string; children: | JSX.Element | JSX.Element[] | string | string[]; };
ですが、数字についてはどうでしょうか?
幸いなことに、React.Child
というReact要素は、文字列、数字を含む標準の型があります。
そこで、下記のように子要素の型を広げることができます。
type Props = { content: string; children?: | React.ReactChild | React.ReactChild[]; };
React.ReactChild|React.ReactChild[]
は、必要な値の幅を与えてくれますが、少し冗長となってます。
ですのでReactNode
は、より簡潔なオプションです。
type Props = { content: string; children?: React.ReactNode; };
ReactNode
では、複数の要素、文字列、数値、フラグメント、ポータルを使用できます。
つまり、children props
に渡すことができるすべての要素の型を表します。
他の例でも、見てみましょう。
下記のコードでは、ReactのReactNode
型を使用して、子コンポーネント内でprops.children
の型を正しく指定しています。
これにより、子コンポーネントがprops.children
を使用する場合に、その値の型が適切に推論され、親コンポーネントから渡された要素が子コンポーネント内で適切に型付けされるようになります。
import React, { ReactNode } from 'react'; type Props = { children: ReactNode; } function ChildComponent(props: Props) { return ( <div> <h2>Child Component</h2> {props.children} </div> ); }
これにより、親コンポーネントから渡された要素が、子コンポーネント内で適切に型付けされ、コンポーネント間で正しく伝達されるようになります。
ReactのReactNode
型は、複数の要素、文字列、数値、フラグメント、ポータルなど、子要素として使用できるすべての要素の型を表します。
したがって、props.children
に渡される要素がどのような型であっても、ReactNode
型を使用することで、その値を適切に型付けすることができます。
型エラーの検証
TypeScriptを使用することで、ReactアプリケーションにおいてPropsの間違ったセットや値の型に対して、コンパイル時に警告を表示させることができます。
例えば、Propsにstring
型が指定されているプロパティに対して数値を渡した場合、TypeScriptはエラーをスローし、このバグを早期に発見することができます。
下記の例では、MyMessage
コンポーネントに、message
プロパティに数値型の値が渡されていますが、Propsのインターフェースでstring
型が指定されているため、TypeScriptはコンパイルエラーをスローします。
function App() { return ( <div className="App"> <header className="App-header"> <MyMessage message={100} /> </header> </div> ); } interface MessageProps { message: string; }; const MyMessage = (props: MessageProps) => { return <div>Welcome to the {props.message} of React and TypeScript. </div>; }
上記のように、TypeScriptによってコンパイル時にエラーが検出されるため、このエラーはコードベースに隠れることなく、開発中に早期に修正することができます。
また、Propsの必要なすべてのプロパティを渡さない場合も、コンパイルエラーが発生します。
読み取り専用プロパティの指定
interfaceのプロパティを読み取り専用としてマークすることもできます。
これを行うには、プロパティのキーの前にreadonly
キーワードを追加します。
interface Props { content: string; totalCost: number; readonly dateCreated: Date; };
読み取り専用プロパティは、オブジェクトが最初に作成されたときにのみ変更可能であり、再割り当てしようとするとコンパイラはエラーをスローします。
このように、読み取り専用プロパティを使用することで、不必要にプロパティが変更されるのを防止し、安全なコーディングを実現することができます。
React.FCとReact.VFCについて
React.FCとReact.VFCは、Reactで関数コンポーネントを記述するための汎用的なインターフェースです。
React.FC
はFunctional Componentの略で、アロー関数を使って書かれたコンポーネントの標準的な型です。
React.VFC
はVoid Function Componentの略で、戻り値がvoidである関数コンポーネントの型です。
以前は、React.FCは常にコンポーネントにchildren propsを含めていましたが、React 18のリリース以降は含まれなくなりました。
つまり、React.FCを使用していても、children propsを明示的に指定しなければなりません。
一方、React.VFCは常に明示的にchildren propsを含めませんでした。
しかし、React 18のリリース以降、React.FCには暗黙の子が含まれなくなり、React.VFCはCRAのテンプレートで非推奨になり削除されました。
現在、children propsを含むかどうかはReact.FCとReact.VFCの違いではなく、コンポーネントの実装によって異なります。
そのため、React.FCとReact.VFCのどちらを使用するかは好みの問題になります。
パフォーマンスへの影響は大きくはありませんが、React.FCおよびReact.VFCは、特にコードベースが大規模になる場合には制限と欠点があります。
したがって、これらの型に依存せず、状況に応じて最適な型を選択することが重要です。
初学者の方は、これらの型について無理に学ぶ必要はありません。
ただし、これらの型が使用されているコードを読む必要がある場合は、その意味を理解しておくことが役立ちます。
最後に
TypeScriptは、Reactでの開発において優れたツール機能と型の安全機能を提供しています。
そのため、Reactで多くの型付けや定型コードを記述する必要はありません。
Reactでは、コンポーネントが非常に重要であり、必要なものが数回のキーストロークで揃います。
そして、ReactコンポーネントはTypeScriptから大きな恩恵を受けることができます。
TypeScriptの型システムは、コンポーネントのPropsを検証する際に特に役立ちます。
この機能により、入力値が適切かどうかを事前に確認することができます。
以上が本日の記事の内容です。
最後まで読んで頂きありがとうございます。
この記事が役に立ったら、ブックマークと共有をしていただけると幸いです。