React Docs（Beta）のチュートリアルで作った三目並べゲームをTypeScript化したメモ

以下の[React Docs（Beta）](https://beta.reactjs.org/)のチュートリアルで作った三目並べゲームを、TypeScript化したメモです。

:::note info
`create-react-app`を使う際、`--template typescript`を指定すれば最初からTypeScriptで作成できますが、この記事ではJavaScriptのReactアプリを作成した後、TypeScript化しています。
:::

https://beta.reactjs.org/learn/tutorial-tic-tac-toe

React Docs（Beta）では、クラスではなく、Hooksを使って書かれており、主流の書き方にアップデートされています。

最終的なコードはこちらで確認できます。

https://github.com/onukichi/react-tutorial-tic-tac-toe/tree/ea4e09c2cc4cc63d6bd5c9b70550bacca804ccad

# やったこと
1. TypeScriptのインストール
2. ファイル拡張子をtsxに変更
3. tsconfig.jsonを作成
4. 型付けする 

## 1. TypeScriptのインストール

```shell
npm install --save typescript @types/node @types/react @types/react-dom
```

## 2. ファイル拡張子をtsxに変更した

```shell
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`を読み込んでいます。

```typescript:src/App.tsx
import Game from './components/Game';

function App() {
  return (
    <div>
      <Game />
    </div>
  );
}

export default App;
```


## 3. tsconfig.jsonを作成

```shell
npx tsc --init
```

tsconfig.jsonが作成されます。当初のtsconfig.jsonは以下のようになっていると思います。

```json:tsconfig.json
{
  "compilerOptions": {
    "target": "es2016",
    "module": "commonjs",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true
  }
}
```

tsconfig.jsonに、`"jsx": "react-jsx"`を追加します。

```json:tsconfig.json
{
  "compilerOptions": {
    "target": "es2016",
    "module": "commonjs",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true,
    "jsx": "react-jsx" /* 追加 */
  }
}
```


## 4. 型付けする

型付けする前のコードはこちらで確認できます。

https://github.com/onukichi/react-tutorial-tic-tac-toe/blob/30c4b5ae2ccb487bc71e22980c04e1cbdade4764/src/components/Game.tsx


型付けは、こちらの記事を参考にさせていただきました。とても分かりやすかったです。ありがとうございました。

https://zenn.dev/roiban/articles/473f9cbf2b793a

型付け後のコードはこのようになりました。コンパイルエラーも消えています。

```typescript:src/components/Game.tsx
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でページごと翻訳して進めました。