Edited at

TypeScriptの型システムなら《コンパイル時に》ポーカーの役を判定できるかも?

与えられた手札(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-playground___Volumes_dev_typescript-playground__-_____logical_ts__typescript-playground_.png

今回の試みでTypeScriptの型システムを用いると、ポーカーの役が判定できることがわかりました。ただ、実用性はないと思います。