与えられた手札(5枚のトランプカード)からポーカーの役を判定するアルゴリズムをどう組むかというクイズあります。
第一回 オフラインリアルタイムどう書くの参考問題 - Qiita
こちらを、TypeScriptの型システムだけで達成できるのかやってみました。
TypeScriptの型システムにはConditional Typeという仕組みがあり、平たく言うと型宣言に条件分岐が使えるというものです。
次のように、A extends B ? C : D
のように三項演算子のような書き方で型の分岐が行えます。
type Exclude<T, U> = T extends U ? never : T
この例では、T
型がU
型と同じだったらnever
型、つまりコンパイルエラーにし、そうでなければコンパイルを通すという仕掛けになります。
type StringOrStrings = string | string[]
type StringOnly = Exclude<StringOrStrings, string[]> // string
上で定義したStringOnly
型はstring
型もしくはstring[]
型を受けつけるユニオン型から、string[]
でない型、つまり、string
型だけを受け付ける型という意味になります。
const value1: StringOrStrings = 'aaa'
const value2: StringOrStrings = ['aaa']
const value3: StringOnly = 'aaa'
const value4: StringOnly = ['aaa'] // コンパイルエラー
このconditional typeの仕組みをフル活用して、コンパイル時にポーカーの役を判定するコードを書いてみました。
次がポーカーの役を判定する型定義部分です。
type A = "A"
type J = "J"
type Q = "Q"
type K = "K"
type Card = A | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | J | Q | K
type True = true
type False = false
type IsTrue<A> = A extends True ? True : False
type Not<A> = A extends True ? False : True
type And<A, B> = A extends True ? IsTrue<B> : False
type Eq<A, B> = A extends B ? True : False;
type FourCard_<A, B, C, D, E> = [A, B, C, D, E] extends [Card, Card, Card, Card, Card]
? And<Eq<A, B>, And<Eq<B, C>, And<Eq<C, D>, Not<Eq<A, E>>>>>
: False;
type FullHouse<A, B, C, D, E> = [A, B, C, D, E] extends [Card, Card, Card, Card, Card]
? And<Eq<A, B>, And<Eq<B, C>, And<Eq<D, E>, Not<Eq<A, E>>>>>
: False;
type ThreeCard<A, B, C, D, E> = [A, B, C, D, E] extends [Card, Card, Card, Card, Card]
? And<Eq<A, B>, And<Eq<B, C>, And<Not<Eq<D, E>>, Not<Eq<A, E>>>>>
: False;
type TwoPair__<A, B, C, D, E> = [A, B, C, D, E] extends [Card, Card, Card, Card, Card]
? And<Eq<A, B>, And<Eq<C, D>, And<Not<Eq<A, C>>, And<Not<Eq<A, E>>, Not<Eq<C, E>>>>>>
: False;
type OnePair__<A, B, C, D, E> = A extends Card
? And<Eq<A, B>, And<Not<Eq<B, C>>, And<Not<Eq<B, D>>, And<Not<Eq<B, E>>,
And<Not<Eq<C, D>>, And<Not<Eq<C, E>>, Not<Eq<D, E>>>>>>>>
: False;
どの役にあたるかの判定はコンパイラにやってもらいます。具体的には上で定義した役の型(FullHouse<A, B, C, D, E>
など)の型パラメータに、手札のランクを当てはめ、コンパイルが通るかで判定します。
const test1: FourCard_<2, 2, 3, J, Q> = true
const test2: FullHouse<2, 2, 3, J, Q> = true
const test3: ThreeCard<2, 2, 3, J, Q> = true
const test4: TwoPair__<2, 2, 3, J, Q> = true
const test5: OnePair__<2, 2, 3, J, Q> = true
この状態で、tsc
でコンパイルを実行します。
tsc poker.ts
logical.ts:29:7 - error TS2322: Type 'true' is not assignable to type 'false'.
29 const test1: FourCard_<2, 2, 3, J, Q> = true
~~~~~
logical.ts:30:7 - error TS2322: Type 'true' is not assignable to type 'false'.
30 const test2: FullHouse<2, 2, 3, J, Q> = true
~~~~~
logical.ts:31:7 - error TS2322: Type 'true' is not assignable to type 'false'.
31 const test3: ThreeCard<2, 2, 3, J, Q> = true
~~~~~
logical.ts:32:7 - error TS2322: Type 'true' is not assignable to type 'false'.
32 const test4: TwoPair__<2, 2, 3, J, Q> = true
~~~~~
Found 4 errors.
コンパイル結果を見るとOnePair__
以外がコンパイルエラーになったので、手札はワンペアであることがわかります。
毎回コンパイルしてどの行が通っているか考えるのは面倒なので、WebStormを使うことをおすすめします。WebStormではコンパイルエラーの箇所が赤の下線で表示され、視覚的に判断しやすいからです。
今回の試みでTypeScriptの型システムを用いると、ポーカーの役が判定できることがわかりました。ただ、実用性はないと思います。