はじめに
どうも私です
先日とある記事を読んでいてTypeScriptで三目並べを型チェックだけで書くことができる
という文言を目にしました
まさかまさかそんなできるわけ...tsならできかねないですね
ということで今回は半分お遊びのマルバツゲームをtypescriptで作ってみたという話です
読んでいた記事
できたもの
できたものがこちらです
おおよそ80行ほどで実装できました(おそらくもっと短く作ることはできますが...)
type Player = "A" | "B";
type Col = 0 | 1 | 2;
type Row = 0 | 1 | 2;
type Square = [Col, Row];
type Board = Square[];
type Play = [Square, Player];
type PlayHistory = Play[];
type PlayerHistory<
B extends PlayHistory,
P extends Player,
History extends Square[] = []
> = B extends [infer First extends Play, ...infer Rest extends Play[]]
? First[1] extends P
? PlayerHistory<Rest, P, [First[0], ...History]>
: PlayerHistory<Rest, P, History>
: History;
type MarkCount<
B extends Board,
D extends 0 | 1,
R extends Row,
Count extends any[] = []
> = B extends [infer First extends Square, ...infer Rest extends Board]
? First[D] extends R
? MarkCount<Rest, D, R, [First, ...Count]>
: MarkCount<Rest, D, R, Count>
: Count["length"];
type IncludesTuple<
T extends readonly any[],
Tuple extends readonly any[]
> = T extends [infer First, ...infer Rest]
? IsSame<First, Tuple> extends true
? true
: IncludesTuple<Rest, Tuple>
: false;
type IsSame<T, U> = [T, U] extends [U, T] ? true : false;
type DiagonalCheck<B extends Board> = IncludesTuple<B, [1, 1]> extends true
? IncludesTuple<B, [0, 0]> extends true
? IncludesTuple<B, [2, 2]> extends true
? true
: false
: IncludesTuple<B, [0, 2]> extends true
? IncludesTuple<B, [2, 0]> extends true
? true
: false
: false
: false;
type MarkCheck<P extends Board, D extends 0 | 1, R extends Row> = MarkCount<
P,
D,
R
> extends 2
? true
: false;
type Check<B extends Board, S extends Square> =
| MarkCheck<B, 0, S[0]>
| MarkCheck<B, 1, S[1]>
| DiagonalCheck<[...B, S]> extends false
? false
: true;
type PlayBoard<PH extends PlayHistory, P extends Play> = Check<
PlayerHistory<PH, P[1]>,
P[0]
> extends true
? `${P[1]} is win`
: [P, ...PH];
type Play1 = PlayBoard<[], [[0, 0], "A"]>;
type Play2 = PlayBoard<Play1, [[1, 0], "B"]>;
type Play3 = PlayBoard<Play2, [[1, 1], "A"]>;
type Play4 = PlayBoard<Play3, [[1, 2], "B"]>;
type Play5 = PlayBoard<Play4, [[2, 2], "A"]>;
Play1
において一番最初に0行0列にAが入ります
A
Play2
において一番最初に1行0列にBが入ります
A
B
Play3
において一番最初に1行1列にAが入ります
A
B A
Play4
において一番最初に1行2列にBが入ります
A B
B A
Play5
において一番最初に1行2列にAが入ります
A B
B A
A
これでAが斜めに三つ揃ったのでAの勝ちです
Play5
にはA is win
の文字が入りそれ以降は型エラーが発生します
基本型
これらはゲームの制約を決める型です
// playerの種類
type Player = "A" | "B";
// 列数
type Col = 0 | 1 | 2;
// 行数
type Row = 0 | 1 | 2;
// マス目
type Square = [Col, Row];
// ゲーム板
type Board = Square[];
// 1ターンの情報
type Play = [Square, Player];
// ゲーム全体の情報
type PlayHistory = Play[];
特定のプレイヤーの過去の手順を取得する型
次に特定のプレイヤーの過去の手順を取得する型PlayerHistory
です
全ての手順情報とPlayerの種類を受け取り全ての手順からそのPlayerに関する手順を返します
手順配列から手順を一つ取り出しそれがPlay型を満たしていない場合(配列に要素がない->never
になるためPlayを満たさない)そのPlayerが今までおいたマス目の情報を返します
満たしていた場合その手順のPlayerが指定されたプレイヤーである場合PlayerHistoryを再起的に呼び出しHistoryにその手順を追加します
指定されたプレイヤーでなかった場合PlayerHistoryを再起的に呼び出します
これにより全ての手順から特定のプレイヤーが打った手順を取得することができます
type PlayerHistory<
B extends PlayHistory,
P extends Player,
History extends Square[] = []
> = B extends [infer First extends Play, ...infer Rest extends Play[]]
? First[1] extends P
? PlayerHistory<Rest, P, [First[0], ...History]>
: PlayerHistory<Rest, P, History>
: History;
指定した行or列を満たす手順が何個あるかカウントする型
次に指定した行or列を満たす手順が何個あるかカウントする型MarkCount
です
こちらはBoard,Direction(0|1), Row(行 or 列の数)を引数に取り指定された行or列を満たす手順の個数を返します
先ほどの型と同様に条件分岐により条件を満たすもののみを配列に追加し、最終的にその配列の長さを返しています
type MarkCount<
B extends Board,
D extends 0 | 1,
R extends Row,
Count extends any[] = []
> = B extends [infer First extends Square, ...infer Rest extends Board]
? First[D] extends R
? MarkCount<Rest, D, R, [First, ...Count]>
: MarkCount<Rest, D, R, Count>
: Count["length"];
縦、横が揃っているかをチェックする型
次に縦、横が揃っているかをチェックする型MarkCheck
です
こちらは先ほどのMarkCountを使って手順の個数を取得しその個数が2であればtrue
そうでなければfalse
を返します
2個である理由は最新の手を配列に追加していないからですね
type MarkCheck<P extends Board, D extends 0 | 1, R extends Row> = MarkCount<
P,
D,
R
> extends 2
? true
: false;
斜めが揃っているかをチェックする型
次に斜めが揃っているかをチェックする型DiagonalCheck
です
こちらはかなり力技で条件分岐をしています
まず中央があるかをみて存在しなければfalse
ある場合は0行0列、2行2列を見て存在しなければfalse
存在すればtrueを返します
同様に0行2列 2行0列も見ます
type DiagonalCheck<B extends Board> = IncludesTuple<B, [1, 1]> extends true
? IncludesTuple<B, [0, 0]> extends true
? IncludesTuple<B, [2, 2]> extends true
? true
: false
: IncludesTuple<B, [0, 2]> extends true
? IncludesTuple<B, [2, 0]> extends true
? true
: false
: false
: false;
その手によってどこかが揃うかをチェックする型
次にその手によってどこかが揃うかをチェックする型Check
です
これは先ほどのDiagonalCheck
とMarkCheck
の論理和を返します
type Check<B extends Board, S extends Square> =
// 行をチェック
| MarkCheck<B, 0, S[0]>
// 列をチェック
| MarkCheck<B, 1, S[1]>
// 斜めをチェック
| DiagonalCheck<[...B, S]> extends false
// 全てfalseならfalseを返す
? false
: true;
ゲーム進行をする型
最後にゲームの進行をする型PlayBoard
です
これはPlayHistory型とPlay型を受け取り、Play型によってどこかが揃った場合は<Player> is win
という文字を返し、そうでない場合は新たにPlayHistoryにPlayを追加したPlayHistory型を返します
type PlayBoard<PH extends PlayHistory, P extends Play> = Check<
PlayerHistory<PH, P[1]>,
P[0]
> extends true
? `${P[1]} is win`
: [P, ...PH];
あとはゲームをするのみ
これらの型を用いて次のように型を書けばゲームが進行します
type Play1 = PlayBoard<[], [[0, 0], "A"]>;
type Play2 = PlayBoard<Play1, [[1, 0], "B"]>;
type Play3 = PlayBoard<Play2, [[1, 1], "A"]>;
type Play4 = PlayBoard<Play3, [[1, 2], "B"]>;
type Play5 = PlayBoard<Play4, [[2, 2], "A"]>;
開発中詰まった点
いきなりですが次の方はどのような型になると思いますか?
type A = true & false extends false ? false : true;
正解はfalseです
では次に次はどうでしょうか
type A = true & false extends true ? false : true;
そりゃあextendsの条件分岐がfalse->trueになるからtrueでしょう
false
です
これは開発中めっちゃ引っ掛かっちゃったんですけど
trueとfalseの交差型(true & false)はfalseではなくneverを返します
頭の中でtrue & falseならfalseでしょ!っていう脳内変換が邪魔しすぎてすっぽ抜けてましたね
普通に考えるとtrueでありfalseである方など存在しません
のでneverになるわけですね
そしてnever型というのは全ての型を継承できるようです(だからさっきの条件分岐でfalse->trueに変えても結果が変わらなかった)
ので変換表としては以下ですね
true & false extends true -> true
true & false extends false -> true
true & true extends true -> true
true | false extends true -> false
true | true extends true -> true
true | false extends boolean -> true
終わりに
今回はみんな大好きtsの型パズルの話でした
今回作った型に関しては実用性がないかもしれませんがconditional typeなどは実務やライブラリ作成などで使うこともあるので覚えておいて損はないと思います!
tsって楽しい
終わり