1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-01-07

以下のReact Docs(Beta)のチュートリアルで作った三目並べゲームを、TypeScript化したメモです。

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

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

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

やったこと

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

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

src/App.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は以下のようになっていると思います。

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

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

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

4. 型付けする

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

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

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

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でページごと翻訳して進めました。
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?