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

与えられた手札(5枚のトランプカード)からポーカーの役を判定するアルゴリズムをどう組むかというクイズあります。

[第一回 オフラインリアルタイムどう書くの参考問題 - Qiita](https://qiita.com/Nabetani/items/cbc3af152ee3f50a822f)

こちらを、TypeScriptの型システムだけで達成できるのかやってみました。

TypeScriptの型システムにはConditional Typeという仕組みがあり、平たく言うと型宣言に条件分岐が使えるというものです。

次のように、`A extends B ? C : D`のように三項演算子のような書き方で型の分岐が行えます。

```typescript
type Exclude<T, U> = T extends U ? never : T
```

この例では、`T`型が`U`型と同じだったら`never`型、つまりコンパイルエラーにし、そうでなければコンパイルを通すという仕掛けになります。

```typescript
type StringOrStrings = string | string[]
type StringOnly = Exclude<StringOrStrings, string[]> // string
```

上で定義した`StringOnly`型は`string`型もしくは`string[]`型を受けつけるユニオン型から、`string[]`でない型、つまり、`string`型だけを受け付ける型という意味になります。


```typescript
const value1: StringOrStrings = 'aaa'
const value2: StringOrStrings = ['aaa']
const value3: StringOnly = 'aaa'
const value4: StringOnly = ['aaa'] // コンパイルエラー
```

このconditional typeの仕組みをフル活用して、**コンパイル時に**ポーカーの役を判定するコードを書いてみました。

次がポーカーの役を判定する型定義部分です。

```typescript
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>`など)の型パラメータに、手札のランクを当てはめ、コンパイルが通るかで判定します。

```typescript
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ではコンパイルエラーの箇所が赤の下線で表示され、視覚的に判断しやすいからです。

<img width="848" alt="typescript-playground___Volumes_dev_typescript-playground__-_____logical_ts__typescript-playground_.png" src="https://qiita-image-store.s3.amazonaws.com/0/889/0d05f2d7-7c37-e4c9-540d-90043446b38d.png">

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