Closed36

TypeScriptでReactのチュートリアルをやる

ikura1ikura1

まずはプロジェクトの生成

npx create-react-app my_app --template typescript
ikura1ikura1

次はチュートリアルの流れで削除

cd src
rm -rf *.*
ikura1ikura1

チュートリアルでコピペで必要なのは、index.tsxindex.cssになる

ikura1ikura1

とりあえずのエラー箇所は二つ

RROR in src/index.tsx:12:16
TS7006: Parameter 'i' implicitly has an 'any' type.
    10 |
    11 | class Board extends React.Component {
  > 12 |   renderSquare(i) {
       |                ^
    13 |     return <Square />;
    14 |   }
    15 |

ERROR in src/index.tsx:60:34
TS2345: Argument of type 'HTMLElement | null' is not assignable to parameter of type 'Element | DocumentFragment'.
  Type 'null' is not assignable to type 'Element | DocumentFragment'.
    58 | // ========================================
    59 |
  > 60 | const root = ReactDOM.createRoot(document.getElementById("root"));
       |                                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    61 | root.render(<Game />);
    62 |
ikura1ikura1

一つ目は、renderSquareの型エラー

anyゆるせん!!

number型付けをして回避

renderSquare(i: number) {

2つ目は、document.getElementById("root")の型エラー

nullが入るのはいかがか!!

HTMLElementのキャストして回避

const root = ReactDOM.createRoot(
  document.getElementById("root") as HTMLElement,
);
ikura1ikura1

Squareクラスにvalueを渡すだけで大騒ぎだ

ERROR in src/index.tsx:13:20

TS2769: No overload matches this call.
  Overload 1 of 2, '(props: {} | Readonly<{}>): Square', gave the following error.
    Type '{ value: number; }' is not assignable to type 'IntrinsicAttributes & IntrinsicClassAttributes<Square> & Readonly<{}>'.
      Property 'value' does not exist on type 'IntrinsicAttributes & IntrinsicClassAttributes<Square> & Readonly<{}>'.
  Overload 2 of 2, '(props: {}, context: any): Square', gave the following error.
    Type '{ value: number; }' is not assignable to type 'IntrinsicAttributes & IntrinsicClassAttributes<Square> & Readonly<{}>'.
      Property 'value' does not exist on type 'IntrinsicAttributes & IntrinsicClassAttributes<Square> & Readonly<{}>'.
    11 | class Board extends React.Component {
    12 |   renderSquare(i: number) {
  > 13 |     return <Square value={i} />;
       |                    ^^^^^
    14 |   }
    15 |
    16 |   render() {
ikura1ikura1

クラスに型を付けることで回避

type SquareProps = {
  value: number;
};
class Square extends React.Component<SquareProps> {

ikura1ikura1

インタラクティブなコンポーネントを作る

onClickconsole.logを設定

class Square extends React.Component<SquareProps> {
  render() {
    return (
      <button className="square" onClick={() => console.log("click")}>
        {this.props.value}
      </button>
    );
  }
}

これは型が必要ないから大丈夫

ikura1ikura1

コンストラクタの追加とstateの初期化なんだけど、そこconstructorの型じゃなかったの…?

class Square extends React.Component<SquareProps> {
  constructor(props) {
class Square extends React.Component<SquareProps> {
  constructor(props) {
    super(props);
    this.state = {
      value: null,
    };
  }
ikura1ikura1

Stateへの型追加とprosへの型宣言の追加

type SquareProps = {
  value: number;
};

type SquareState = {
  value: string | null;
};

class Square extends React.Component<SquareProps, SquareState> {
  constructor(props: SquareProps) {
    super(props);
    this.state = {
      value: null,
    };
  }

良いものを見付けた(初期化のみならチートシートと同じくconstructorいらんよな…
https://react-typescript-cheatsheet.netlify.app/docs/basic/getting-started/class_components/

ikura1ikura1

Stateのリフトアップ

Boardクラスにconstructorを追加
propsは側だけ

type BoardProps = {};

type BoardState = {
  squares: string | null[];
};

class Board extends React.Component<BoardProps, BoardState> {
  constructor(props: BoardProps) {
    super(props);
    this.state = {
      squares: Array(9).fill(null),
    };
  }

ikura1ikura1

SquarePropsがnumberだったのでエラーはいた

ERROR in src/index.tsx:45:20

TS2322: Type 'string | null' is not assignable to type 'number'.
  Type 'null' is not assignable to type 'number'.
    43 |
    44 |   renderSquare(i: number) {
  > 45 |     return <Square value={this.state.squares[i]} />;
       |                    ^^^^^
    46 |   }
    47 |
    48 |   render() {
ikura1ikura1

valueの型をstring | nullにしてヨシッ!

  renderSquare(i: number) {
    return <Square value={this.state.squares[i]} />;
  }
type SquareProps = {
  value: string | null;
};
ikura1ikura1

BoardからonClickを渡すように変更
SquareProps型にonClickの関数を追加

type SquareProps = {
  value: string | null;
  onClick: () => void;
};
  renderSquare(i: number) {
    return (
      <Square
        value={this.state.squares[i]}
        onClick={() => this.handleClick(i)}
      />
    );
  }
ikura1ikura1
  • Squareからconstructorを削除
  • SquareStateを削除
  • handleClickを追加
class Square extends React.Component<SquareProps> {
  render() {
    return (
      <button className="square" onClick={() => this.props.onClick()}>
        {this.props.value}
      </button>
    );
  }
}
  handleClick(i: number) {
    const squares = this.state.squares.slice();
    squares[i] = "X";
    this.setState({ squares: squares });
  }
ikura1ikura1

関数コンポーネント

function Square(props: SquareProps) {
  return (
    <button className="square" onClick={props.onClick}>
      {props.value}
    </button>
  );
}
ikura1ikura1

手番の処理

  • BoardState型にxIsNextを追加
type BoardState = {
  squares: Array<string | null>;
  xIsNext: boolean;
};

class Board extends React.Component<BoardProps, BoardState> {
  constructor(props: BoardProps) {
    super(props);
    this.state = {
      squares: Array(9).fill(null),
      xIsNext: true,
    };
  }

  handleClick(i: number) {
    const squares = this.state.squares.slice();
    squares[i] = this.state.xIsNext ? "X" : "O";
    this.setState({ squares: squares, xIsNext: !this.state.xIsNext });
  }
  render() {
    const status = `Next player: ${this.state.xIsNext}`;
ikura1ikura1

ゲーム勝者の判定

  • 勝利判定関数の追加
  • Squares型を追加してまとめてる
  handleClick(i: number) {
    const squares = this.state.squares.slice();
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    squares[i] = this.state.xIsNext ? "X" : "O";
    this.setState({ squares: squares, xIsNext: !this.state.xIsNext });
  }
  render() {
    const winner = calculateWinner(this.state.squares);
    let status;
    if (winner) {
      status = "Winner: " + winner;
    } else {
      status = "Next player: " + (this.state.xIsNext ? "X" : "O");
    }
type Squares = Array<string | null>;

type BoardState = {
  squares: Squares;
  xIsNext: boolean;
};

function calculateWinner(squares: Squares) {
  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;
}
ikura1ikura1

Stateのリフトアップ、再び

Gameにconstructorを追加

type GameProps = {};

type GameState = {
  history: Array<{ squares: Squares }>;
  xIsNext: boolean;
};
class Game extends React.Component<GameProps, GameState> {
  constructor(props: GameProps) {
    super(props);
    this.state = {
      history: [
        {
          squares: Array(9).fill(null),
        },
      ],
      xIsNext: true,
    };
  }
ikura1ikura1

Gameの変更に伴うBoardの修正

  • Boardのconstructorの削除
  • Boardのstate参照をpropsに変更
  • handleClickを削除
  • BoardState型を削除
  • BoardProps型にonClickを追加
type BoardProps = {
  squares: Squares;
  onClick: (i: number) => void;
};


class Board extends React.Component<BoardProps> {
  renderSquare(i: number) {
    return (
      <Square
        value={this.props.squares[i]}
        onClick={() => this.props.onClick(i)}
      />
    );
  }

  render() {
    return (
      <div>
        <div className="board-row">
          {this.renderSquare(0)}
          {this.renderSquare(1)}
          {this.renderSquare(2)}
        </div>
        <div className="board-row">
          {this.renderSquare(3)}
          {this.renderSquare(4)}
          {this.renderSquare(5)}
        </div>
        <div className="board-row">
          {this.renderSquare(6)}
          {this.renderSquare(7)}
          {this.renderSquare(8)}
        </div>
      </div>
    );
  }
}
ikura1ikura1

Gameで最新状態が表示されるように変更

  • historyから最新を取得
  • BoardにonClickを渡す
  • stateの表示
  render() {
    const history = this.state.history;
    const current = history[history.length - 1];
    const winner = calculateWinner(current.squares);

    let status: string;
    if (winner) {
      status = `Winner: ${winner}`;
    } else {
      status = `Next player: ${this.state.xIsNext ? "X" : "O"}`;
    }
    return (
      <div className="game">
        <div className="game-board">
          <Board
            squares={current.squares}
            onClick={(i) => this.handleClick(i)}
          />
        </div>
        <div className="game-info">
          <div>{status}</div>
          <ol>{/* TODO */}</ol>
        </div>
      </div>
    );
  }
ikura1ikura1

GameにhandleClickを追加

  handleClick(i: number) {
    const history = this.state.history;
    const current = history[history.length - 1];
    const squares = current.squares.slice();
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    squares[i] = this.state.xIsNext ? "X" : "O";
    this.setState({
      history: history.concat([{ squares: squares }]),
      xIsNext: !this.state.xIsNext,
    });
  }
ikura1ikura1

過去の着手の表示

  • 過去着手の表示を追加
  • 仮で関数を追加
  jumpTo(step: number) {}

  render() {
    const history = this.state.history;
    const current = history[history.length - 1];
    const winner = calculateWinner(current.squares);

    const moves = history.map((_, move) => {
      const desc = move ? `Go to move #${move}` : `Go to game start`;
      return (
        <li>
          <button onClick={() => this.jumpTo(move)}>{desc}</button>
        </li>
      );
    });

    let status: string;
    if (winner) {
      status = `Winner: ${winner}`;
    } else {
      status = `Next player: ${this.state.xIsNext ? "X" : "O"}`;
    }
    return (
      <div className="game">
        <div className="game-board">
          <Board
            squares={current.squares}
            onClick={(i) => this.handleClick(i)}
          />
        </div>
        <div className="game-info">
          <div>{status}</div>
          <ol>{moves}</ol>
        </div>
      </div>
    );
  }
ikura1ikura1

likeyを設定

    const moves = history.map((step, move) => {
      const desc = move ? `Go to move #${move}` : `Go to game start`;
      return (
        <li key={move}>
          <button onClick={() => this.jumpTo(move)}>{desc}</button>
        </li>
      );
    });
ikura1ikura1

GameにstepNumberを追加

  • GameState型にstepNumber: numberを追加
type GameState = {
  history: Array<{ squares: Squares }>;
  stepNumber: number;
  xIsNext: boolean;
};


class Game extends React.Component<GameProps, GameState> {
  constructor(props: GameProps) {
    super(props);
    this.state = {
      history: [
        {
          squares: Array(9).fill(null),
        },
      ],
      stepNumber: 0,
      xIsNext: true,
    };
  }
ikura1ikura1

jumpToの機能を実装

  jumpTo(step: number) {
    this.setState({
      stepNumber: step,
      xIsNext: step % 2 === 0,
    });
  }

ikura1ikura1

handleClickで過去の状態に移動できるように変更

handleClick(i: number) {
    const history = this.state.history.slice(0, this.state.stepNumber + 1);
    const current = history[history.length - 1];
    const squares = current.squares.slice();
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    squares[i] = this.state.xIsNext ? "X" : "O";
    this.setState({
      history: history.concat([{ squares: squares }]),
      stepNumber: history.length,
      xIsNext: !this.state.xIsNext,
    });
ikura1ikura1

renderstepNumberの盤面を参照するように変更

  render() {
    const history = this.state.history;
    const current = history[this.state.stepNumber];
    const winner = calculateWinner(current.squares);

ikura1ikura1

これにてtypescriptでのReactチュートリアルDone

別途確認したいこと

  • useStateを組込むとどうなるのか
  • 空の型を定義したが、直接{}と定義した方がよかったか
このスクラップは2022/08/10にクローズされました