サイトトップ

Director Flash 書籍 業務内容 プロフィール

HTML5テクニカルノート

Create React App + TypeScript: 関数コンポーネントにTypeScriptで型づけする


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.tsxsrc/index.tsx)の拡張子がTypeScript形式の.tsxになっています(図001)。テスト用のモジュール(.test.tsx)やロゴのSVGファイル(logo.svg)は使いません。

図001■ReactアプリケーションのモジュールがTypeScript形式でつくられる

図001

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■関数コンポーネントの引数に警告が示される。

図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.tsx

type 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.tsx

type 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.tsx

function 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.jsoncompilerOptionsで、allowSyntheticDefaultImportstrueに定めておかなければなりません。そして、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.