TypeScriptの型のみでシンプルなOXゲームを作ってみた

# TypeScriptの型のみでシンプルなOXゲームを作ってみた
先日見かけたこの記事に衝撃を受けた。自由な型を設計できるTypeScriptのとりこになった。(けんた食堂さん風)

https://zenn.dev/ame_x/articles/8ec1ec35cdc392

自分も型のみで何か作ってみたいと思いシンプルなOXゲームを実装しました。

## コード
色々な型を組み合わせることで実現しました。そこまで高度なことはしていないと思うので、今回使った文法のみメモします。

```ts
/**
 * 空のマス型
 */
type EmptyCell = " ";

/**
 * ユーザー型
 */
type Player = "O" | "X";

/**
 * 指定できるインデックス型
 */
type Index = 0 | 1 | 2

/**
 * ポジション型
 */
type Position = [Index, Index]

/**
 * マス型
 */
type Cell = Player | EmptyCell

/**
 * 盤面型
 */
type Board = [
  [Cell, Cell, Cell],
  [Cell, Cell, Cell],
  [Cell, Cell, Cell]
]

/**
 * 初期の盤面型
 */
type InitialBoard = [
  [" ", " ", " "],
  [" ", " ", " "],
  [" ", " ", " "]
]

/**
 * マスが埋まっているか調べる型
 */
type GetCell<B extends Board, P extends Position> = B[P[0]][P[1]] extends " " ? true : false

/**
 * マスを指定して変更した盤面を返す型
 */
type SetCell<B extends Board, P extends Position, C extends Player> = {
  0: P extends [0, 0] ? [[C, B[0][1], B[0][2]], B[1], B[2]] : never,
  1: P extends [0, 1] ? [[B[0][0], C, B[0][2]], B[1], B[2]] : never,
  2: P extends [0, 2] ? [[B[0][0], B[0][1], C], B[1], B[2]] : never,
  3: P extends [1, 0] ? [B[0], [C, B[1][1], B[1][2]], B[2]] : never,
  4: P extends [1, 1] ? [B[0], [B[1][0], C, B[1][2]], B[2]] : never,
  5: P extends [1, 2] ? [B[0], [B[1][0], B[1][1], C], B[2]] : never,
  6: P extends [2, 0] ? [B[0], B[1], [C, B[2][1], B[2][2]]] : never,
  7: P extends [2, 1] ? [B[0], B[1], [B[2][0], C, B[2][2]]] : never,
  8: P extends [2, 2] ? [B[0], B[1], [B[2][0], B[2][1], C]] : never,
}

/**
 * never型を取り除くユーティリティ型
 */
type ValueOf<T> = T[keyof T]

/**
 * 行の勝利型
 */
type CheckRowWin<B extends Board, I extends Index, C extends Player> = B[I][0] extends C ? B[I][1] extends C ? B[I][2] extends C ? true : false : false : false


/**
 * 列の勝利型
 */
type CheckColWin<B extends Board, I extends Index, C extends Player> = B[0][I] extends C ? B[1][I] extends C ? B[2][I] extends C ? true : false : false : false


/**
 * 対角線の勝利型
 */
type CheckDiagonWin<B extends Board, C extends Player> =
  (
    B[0][0] extends C
    ? B[1][1] extends C
    ? B[2][2] extends C
    ? true
    : false
    : false
    : false
  ) extends true ? true
  : (B[0][2] extends C
    ? B[1][1] extends C
    ? B[2][0] extends C
    ? true
    : false
    : false
    : false
  )

/**
 * 
 */
type CheckAllWin<B extends Board, C extends Player> =
  CheckRowWin<B, 0, C> extends true ? true :
  CheckRowWin<B, 1, C> extends true ? true :
  CheckRowWin<B, 2, C> extends true ? true :
  CheckColWin<B, 0, C> extends true ? true :
  CheckColWin<B, 1, C> extends true ? true :
  CheckColWin<B, 2, C> extends true ? true :
  CheckDiagonWin<B, C> extends true ? true :
  false

/**
 * プレイする型
 */
type Play<B extends Board, P extends Position, C extends Player> = {
  board: GetCell<B, P> extends true ? ValueOf<SetCell<B, P, C>> : "すでに埋まっています",
  player: C
}

/**
 * 盤面を見やすいようにオブジェクト形式に変換する型
 */
type ConvertBoard<B extends Board> = {
  0: [B[0][0], B[0][1], B[0][2]],
  1: [B[1][0], B[1][1], B[1][2]],
  2: [B[2][0], B[2][1], B[2][2]],
}

/**
 * 勝利した場合、勝利文字列を返することで型エラーを起こす
 */
type CheckUserWin<B extends Board, P extends Position, C extends Player> =
  CheckAllWin<B, C> extends true ? {
    board: B,
    player: C,
    status: `${C}が勝利しました`,
  } : Play<B, P, C>

type Game = CheckUserWin<InitialBoard, [0, 0], "O">
type Game1 = CheckUserWin<Game["board"], [1, 0], "X">
type Game2 = CheckUserWin<Game1["board"], [0, 1], "O">
type Game3 = CheckUserWin<Game2["board"], [1, 1], "X">
type Game4 = CheckUserWin<Game3["board"], [0, 2], "O">

type FinalResult = ConvertBoard<Game4["board"]>
```

## 実際の画面
### エラーなし
![tip-tap.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3865740/389e467b-07af-4b4e-8c93-e24fe7e5b6d2.png)

### 重複エラー
![exist-error.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3865740/d7524beb-96b9-4469-9a45-2582e0618015.png)

### 勝利エラー
![win-error.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3865740/c1696b9d-e973-4b55-a4c0-b63feb537fe3.png)

## 主な文法
### ユニオン型
`|`で複数の型をすることができます
```ts
type Player = "O" | "X";
```

### タプル
固定長の配列を表現します
```ts
type Position = [Index, Index]
```

### Mapped Typesぽいもん
オブジェクトの各プロパティを変換して新しいオブジェクト型を作成しました。
```ts
type ConvertBoard<B extends Board> = { 0: [B[0][0], B[0][1], B[0][2]], ... }
```

### 条件分岐
`extends`と三項演算子を用いることで条件分岐を行う。
```ts
type GetCell<B extends Board, P extends Position> = B[P[0]][P[1]] extends " " ? true : false
```

## 参考

https://zenn.dev/ame_x/articles/8ec1ec35cdc392

## 最後に
間違っていることがあれば、コメントに書いていただけると幸いです。
よろしくお願いいたします。