HTML5テクニカルノート
Create React App + TypeScript: 関数コンポーネントにTypeScriptで型づけする
- ID: FN2006001
- Technique: ECMAScript 2015
- Library: React 16.13.1 / TypeScript: 3.7.2
Reactアプリケーションにデータ型を定めたいとき、TypeScriptが標準として使われるようになってきました。本稿では、関数コンポーネントで組み立てたサンプルを、TypeScriptにより型づけしてみます。
お題として採り上げるのは「React Hooks: クラスのコンポーネントをuseState()で関数に書き替える」で、React公式サイトのチュートリアルにもとづいて、コンポーネントはモジュール分けしたうえでフックを組み込んだ作例(○×ゲーム)です(サンプル001)。このReactアプリケーションに、TypeScriptの環境を組み込んで型づけします。
サンプル001■React Hooks: Tic Tac Toe 02
01 Create React AppでTypeScriptが加わったひな形アプリケーションをつくる
Create React Appは、ひな形のReactアプリケーションを環境とともに構築します。コマンドラインインタフェース(CLI)からnpx create-react-app
と入力して、ディレクトリとなるアプリケーション名を添えるのが基本です。さらに、オプションとして--template typescript
を加えれば、TypeScriptの環境が簡単に加わります(「Adding TypeScript」参照)[*1]。
npx create-react-app react-typescript --template typescript
新たにつくられたディレクトリ(react-typescript
)を開いてみると、アプリケーションを組み立てるふたつのモジュール(src/App.tsx
とsrc/index.tsx
)の拡張子がTypeScript形式の.tsx
になっています(図001)。テスト用のモジュール(.test.tsx
)やロゴのSVGファイル(logo.svg
)は使いません。
図001■ReactアプリケーションのモジュールがTypeScript形式でつくられる
package.json
ファイルには、TypeScriptと型定義が加えられています。
package.json{ "dependencies": { "@types/jest": "^24.0.0", "@types/node": "^12.0.0", "@types/react": "^16.9.0", "@types/react-dom": "^16.9.0", "typescript": "~3.7.2" }, }
ひな形のアプリケーションのインストールに用いられたのは、npmでなくyarnです。ローカルサーバーで開くには、アプリケーションのディレクトリ(react-typescript
)でつぎのコマンドを打ち込んでください。
yarn start
ひな形アプリケーションのモジュールは、前掲サンプル001に合わせてつぎのようなパスの変更およびファイルの追加を行います。
モジュールsrc/App.css src/App.tsx
→src/components/App.css src/components/App.tsx src/components/Board.tsx src/components/Square.tsx
src/index.tsx
のコードは、src/components/App.tsx
のパスの変更に合わせて、つぎのようにimport
を書き替えてください。このモジュールの手直しはこれだけです。とくに型指定しなくても、TypeScriptの型チェックはとおります。
src/index.tsx// import App from './App'; import App from './components/App';
上記4つのファイルに、前掲サンプル001のコードをコピー&ペーストしたうえで、TypeScriptファイルについては次項以降で型づけをしてゆきます。もし、その前に動作を確かめたいという場合、TypeScript形式(.tsx
)では型チェックが働いて、ローカルサーバーを起ち上げられません。そのときは、いったん拡張子をJavaScript形式(.js
)に変えてから、コマンドyarn start
を実行してください(型づけをするときに.tsx
に戻します)。
[*1] 検索で少し前の記事を探すと、オプションとして--template typescript
でなく--typescript
を用いていることがあります。けれど、このオプションは非推奨になりました。実際、このオプションでnpx create-react-app
コマンドを実行すれば、つぎのような警告が表示されるはずです。
The
--typescript
option has been deprecated and will be removed in a future release. In future, please use--template typescript
.
--typescript
オプションは推奨されず、将来のリリースから除かれます。これからは--template typescript
をお使いください。(筆者訳)
↩
02 関数コンポーネントに型づけする
コードの短いモジュールから、順に進めてゆきましょう。src/components/Square.tsx
にサンプル001のsrc/components/Square.js
のコードをコピー&ペーストすると、早速関数コンポーネントSquare
の引数について、つぎのような警告が示されるはずです(図002)。TypeScriptはデータ型を代入や文脈などから推論します。このコンポーネントの引数についてはそれができずに、すべて受け入れるany
になってしまうということです。
(parameter) props: any パラメーター 'props' の型は暗黙的に 'any' になります。
図002■関数コンポーネントの引数に警告が示される。
関数コンポーネントをまずReact.FunctionComponent
インタフェースで定め、あとに添えた山かっこ<>
に与えるのが引数の型です(「関数コンポーネント(Functional Components)」参照)。引数はオブジェクトで、文字列(value
)とメソッド(onClick()
)をひとつずつもちます。こうした型を定めるのが型エイリアスtype
です(「型エイリアス(Type Alias)」参照)。オブジェクトリテラルと同じようにプロパティを加え、値の替わりにデータ型を与えてください。なお、React.FunctionComponent
は、React.FC
と短く書くこともできます(「Function Components」参照)。
モジュールsrc/components/Square.tsx
の関数コンポーネント(Square
)を型づけしたのが、つぎに抜き書きしたコードです。エイリアスtype
でオブジェクト(Props
)にメソッド(onClick
)を定めるには、引数と戻り値の型を与えてください。
src/components/Square.tsxtype Props = { value: string, onClick: () => void }; // const Square = (props) => { const Square: React.FC<Props> = (props) => { };
つづいて、モジュールsrc/components/Board.tsx
の型づけも基本的に同じです。type
でオブジェクト(Props
)の型に定めるArray
のプロパティ(squares
)については、つづく<>
に配列要素の型を与えます。
src/components/Board.tsxtype Props = { squares: Array<string>, finished: boolean, onClick: (i: number) => void }; // const Board = (props) => { const Board: React.FC<Props> = (props) => { };
ふたつのモジュールについて、TypeScriptに対応する書き替えはこれだけです。関数コンポーネントのインタフェースReact.FunctionComponent
(React.FC
)には、JSX要素のテンプレートを返すことも定められています。以下のコード001と002に、それぞれのモジュールの記述をまとめました。
コード001■src/components/Square.tsx
import React from 'react';
type Props = {
value: string,
onClick: () => void
};
const Square: React.FC<Props> = (props) => {
return (
<button className="square" onClick={props.onClick}>
{props.value}
</button>
);
};
export default Square;
コード002■src/components/Board.tsx
import React from 'react';
import Square from './Square';
type Props = {
squares: Array<string>,
finished: boolean,
onClick: (i: number) => void
};
const Board: React.FC<Props> = (props) => {
const renderSquare = (i: number) =>
<Square
value={props.squares[i]}
onClick={() => props.onClick(i)}
/>
return (
<div>
<div className="board-row">
{renderSquare(0)}
{renderSquare(1)}
{renderSquare(2)}
</div>
<div className="board-row">
{renderSquare(3)}
{renderSquare(4)}
{renderSquare(5)}
</div>
<div className="board-row">
{renderSquare(6)}
{renderSquare(7)}
{renderSquare(8)}
</div>
</div>
);
};
export default Board;
03 アプリケーションモジュールのメソッドに型づけする
アプリケーションモジュールsrc/components/App.js
の関数コンポーネントは、function
宣言で定めました。引数はありませんし、戻り値もreturn
した値から型推論されます。ですから、コンポーネントそのものへの型づけは要りません(JSX要素で型指定したい場合にはReact.ReactElement
が使えます)。
けれど、コンポーネントのメソッドや、外に定めた関数については、引数の型を与えるよう求められるでしょう。型に配列を定めるときは、Array<>
の中に要素の型を加えてください。
src/components/App.tsxfunction App() { // const handleClick = i => { const handleClick = (i: number) => { }; // const jumpTo = step => { const jumpTo = (step: number) => { }; } // function calculateWinner(squares) { function calculateWinner(squares: Array<string>) { }
これでTypeScript形式(.tsx
)のモジュールが、すべて型チェックをとおるようになりました。モジュールsrc/components/App.tsx
の記述全体は、つぎのコード003のとおりです。併せて、CodeSandboxに以下のサンプル002を掲げますので、各モジュールのコードと動きについてはこちらでお確かめください。
コード003■src/components/App.tsx
import React, { useState, ReactElement } from 'react';
import Board from './Board';
import './App.css';
function App() {
const [history, setHistory] = useState([{ squares: new Array(9) }]);
const [stepNumber, setStepNumber] = useState(0);
const [xIsNext, setXIsNext] = useState(true);
const [finished, setFinished] = useState(false);
const handleClick = (i: number) => {
if (finished) {
return;
}
if (stepNumber >= 9) {
setFinished(true);
return;
}
const _history = history.slice(0, stepNumber + 1);
const squares = [..._history[_history.length - 1].squares];
console.log('history:', _history.length, stepNumber);
if (squares[i]) {
return;
}
const winner = calculateWinner(squares);
if (winner) {
setFinished(true);
return;
}
squares[i] = xIsNext ? 'X' : 'O';
setHistory([..._history, { squares }]);
setStepNumber(_history.length);
setXIsNext(!xIsNext);
};
const jumpTo = (step: number) => {
setStepNumber(step);
setXIsNext(step % 2 === 0);
setFinished(false);
};
const _history = [...history];
const squares = [..._history[stepNumber].squares];
const winner = calculateWinner(squares);
const status = winner
? 'Winner: ' + winner
: 'Next player: ' + (xIsNext ? 'X' : 'O');
const moves = _history.map((step, move) => {
const desc = move ? 'Go to move #' + move : 'Go to game start';
return (
<li key={move}>
<button onClick={() => jumpTo(move)}>{desc}</button>
</li>
);
});
return (
<div className="game">
<Board
squares={squares}
finished={finished}
onClick={i => handleClick(i)}
/>
<div className="game-info">
<div>{status}</div>
<ol>{moves}</ol>
</div>
</div>
);
}
function calculateWinner(squares: Array<string>) {
const lines = [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[0, 3, 6],
[1, 4, 7],
[2, 5, 8],
[0, 4, 8],
[2, 4, 6]
];
const length = lines.length;
for (let i = 0; i < length; i++) {
const [a, b, c] = lines[i];
const player = squares[a];
if (player && player === squares[b] && player === squares[c]) {
return player;
}
}
return null;
}
export default App;
サンプル002■React + TypeScript: Tic Tac Toe
04 Reactライブラリのimport
お題のサンプル書き替えはここまでにして、ふたつほど補います。まず、ひとつ目はReactライブラリをimport
する構文です。TypeScript形式のコード例で、つぎのように書かれていることがあります。この構文でも構いません。
import * as React from 'react';
けれど、TypeScript 2.7から、前掲各コードのようにJavaScript形式と同じimport
の構文が使えるようになりました(「Import React」)。ただし、その場合にはtsconfig.json
のcompilerOptions
で、allowSyntheticDefaultImports
をtrue
に定めておかなければなりません。そして、create-react-app
で、--template typescript
オプションを添えてつくったひな形アプリケーションにはこの設定が加わっているのです。
tsconfig.json{ "compilerOptions": { "allowSyntheticDefaultImports": true, }, }
05 typeとinterface
もうひとつ触れておきたいのは、プロパティが備わったオブジェクトを、どう型指定するかです。本稿では型エイリアスtype
で定めました。けれど、interface
によりインタフェースをつくる手もあります。TypeScriptの公式サイトを見ると、拡張性の観点からはinterface
を勧めるようです(「Interfaces vs. Type Aliases」)。そのほか、複数のモジュールで用いる共通の型を決めるようなときも、インタフェースの方がよいでしょう。けれど、拡張はあまり考えず、それで済むのであれば、型エイリアスでも構わないとされています。
src/components/Square.tsx// type Props = { interface IProps { value: string, onClick: () => void }; // const Square: React.FC<Props> = (props) => { const Square: React.FC<IProps> = (props) => { };
今回のお題では、それぞれのコンポーネントの中だけで使う簡単な型の定めです。型エイリアスtype
で差し支えないでしょう。
作成者: 野中文雄
作成日: 2020年05月30日
Copyright © 2001-2020 Fumio Nonaka. All rights reserved.