以下のReact Docs(Beta)のチュートリアルで作った三目並べゲームを、TypeScript化したメモです。
create-react-app
を使う際、--template typescript
を指定すれば最初からTypeScriptで作成できますが、この記事ではJavaScriptのReactアプリを作成した後、TypeScript化しています。
React Docs(Beta)では、クラスではなく、Hooksを使って書かれており、主流の書き方にアップデートされています。
最終的なコードはこちらで確認できます。
やったこと
- TypeScriptのインストール
- ファイル拡張子をtsxに変更
- tsconfig.jsonを作成
- 型付けする
1. TypeScriptのインストール
npm install --save typescript @types/node @types/react @types/react-dom
2. ファイル拡張子をtsxに変更した
mv src/App.js src/App.tsx
mv src/index.js src/index.tsx
チュートリアルでは、src/App.js
に全てのコンポーネントを記述しています。それでも問題はありませんが、後ほどコンパイルエラーを修正する際に煩わしかったので、私はsrc/components
ディレクトリを切り、src/components/Game.tsx
ファイルを作成し、コンポーネントをそちらに移しました。src/App.tsx
で、src/components/Game.tsx
を読み込んでいます。
import Game from './components/Game';
function App() {
return (
<div>
<Game />
</div>
);
}
export default App;
3. tsconfig.jsonを作成
npx tsc --init
tsconfig.jsonが作成されます。当初のtsconfig.jsonは以下のようになっていると思います。
{
"compilerOptions": {
"target": "es2016",
"module": "commonjs",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
}
}
tsconfig.jsonに、"jsx": "react-jsx"
を追加します。
{
"compilerOptions": {
"target": "es2016",
"module": "commonjs",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"jsx": "react-jsx" /* 追加 */
}
}
4. 型付けする
型付けする前のコードはこちらで確認できます。
型付けは、こちらの記事を参考にさせていただきました。とても分かりやすかったです。ありがとうございました。
型付け後のコードはこのようになりました。コンパイルエラーも消えています。
import { Repeat } from 'typescript-tuple'
import { useState } from 'react';
import '../styles.css';
type SquareValue = 'O' | 'X' | null
type SquareProps = {
value: SquareValue
onSquareClick: () => void
}
function Square(props: SquareProps) {
const { value, onSquareClick } = props
return <button className="square" onClick={onSquareClick}>{ value }</button>;
}
function calculateWinner(squares: BoardState) {
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]
];
for(let i = 0; i < lines.length; i++) {
const [a, b, c] = lines[i]
if(squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
return squares[a]
}
}
return null
}
type BoardState = Repeat<SquareValue, 9>
type BoardProps = {
xIsNext: boolean
squares: BoardState
onPlay: (nextSquares: BoardState) => void
}
function Board(props: BoardProps) {
const { xIsNext, squares, onPlay } = props
function handleClick(i: number) {
if(squares[i] || calculateWinner(squares)) {
return
}
const nextSquares = squares.slice();
if(xIsNext) {
nextSquares[i] = "X"
} else {
nextSquares[i] = "O"
}
onPlay(nextSquares as BoardState)
}
const winner = calculateWinner(squares)
let status
if(winner) {
status = "Winner: " + winner
} else {
status = "Next player: " + (xIsNext ? "X" : "O")
}
return (
<>
<div className="status">{status}</div>
<div className="board-row">
<Square value={squares[0]} onSquareClick={() => handleClick(0)} />
<Square value={squares[1]} onSquareClick={() => handleClick(1)} />
<Square value={squares[2]} onSquareClick={() => handleClick(2)} />
</div>
<div className="board-row">
<Square value={squares[3]} onSquareClick={() => handleClick(3)} />
<Square value={squares[4]} onSquareClick={() => handleClick(4)} />
<Square value={squares[5]} onSquareClick={() => handleClick(5)} />
</div>
<div className="board-row">
<Square value={squares[6]} onSquareClick={() => handleClick(6)} />
<Square value={squares[7]} onSquareClick={() => handleClick(7)} />
<Square value={squares[8]} onSquareClick={() => handleClick(8)} />
</div>
</>
);
}
export default function Game() {
const [history, setHistory] = useState<BoardState[]>([[null, null, null, null, null, null, null, null, null]])
const [currentMove, setCurrentMove] = useState<number>(0)
const xIsNext: boolean = currentMove % 2 === 0
const currentSquares: BoardState = history[currentMove]
function handlePlay(nextSquares: BoardState) {
const nextHistory = [...history.slice(0, currentMove + 1), nextSquares]
setHistory(nextHistory)
setCurrentMove(nextHistory.length - 1)
}
function jumpTo(nextMove: number) {
setCurrentMove(nextMove)
}
const moves = history.map((squares, move) => {
let description: string;
if(move > 0) {
description = 'Go to move #' + move
} else {
description = 'Go to game start'
}
return (
<li key={move}>
<button onClick={() => jumpTo(move)}>{ description }</button>
</li>
)
});
return (
<div className="game">
<div className="game-board">
<Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} />
</div>
<div className="game-info">
<ol>{ moves }</ol>
</div>
</div>
)
}
最後に
- TypeScript無関係ですが、既存のReact DocsよりもReact Docs(Beta)のチュートリアルの方が、分かりやすいと感じました。
- またクラスコンポーネントではなく、関数コンポーネントで書かれているので、普段の実務の書き方に近いためしっくり来ました。
- 難点としてはまだ日本語化されていない点で、私はDeepLでページごと翻訳して進めました。