TypeScriptのUnionの分配ルール (Distributive Conditional Types)
意外と知らなかったのでメモ。
なにかというと、結論これを見てもらったら早いです。
https://scrapbox.io/files/6528f60ffc8884001b8a3e2f.png
Genericsで受け取ったTに対してConditional Typeを使うと、自動的にUnionの要素それぞれに対してGenericsが通ったような形に分配される仕様。
多くの場合はこの挙動でうまく動いていて使う側は自然すぎて気づかない。しかし型を組む側に立つと、時にはすこし奇妙にも感じる。
ドキュメントはココ。
別の例だと、Tが 'a' | 'b' のときにtrueにしたいような型を作ってみる。
ナイーブに書くとこんな感じになると思う。
たしかにUnionではないケースではうまく動いているように見える。ただし、Unionが入ってくると true か false しか返らないはずと思っていたところが、ABCの例では boolean が返ってきていたりする。
ABCの部分、これはDistributionが起きていて ISAorB<'a'> | ISAorB<'b'> | ISAorB<'c'> のような評価が行われて、 true | true | false が返ってきてしまった結果である。
https://scrapbox.io/files/6528fea3e337a2001cb3bea6.png
面白いのが never が入ってきたとき。never は空集合つまり、すべての型に対して部分集合になり得るので、なのでぱっと見では true が返って来そうではある。しかしながら、得られたのは true でもなく false でもなく never。
これはDistributionが起きていて、空集合が分配されたというような評価になっている。つまりゼロ個のUnion、never になる。
先の配列の例でも、同様にneverが返ってきている。これも同様の評価を辿っている。
https://scrapbox.io/files/6528fa01e1e9de001b5bbb67.png
話を戻すと、このような Distribution が行われてしまうと IsAorB では思ったような得られなかった。となると、この Distribution を回避するためによく知られた方法があった。
https://scrapbox.io/files/652901d2c7997d001b30e788.png
TをTupleでくくってあげるという方法。かなりトリッキーだが、たしかに思ったような動きをしているように見える。
TODO: なぜこの挙動になっているのか調べる
これを応用するとUnionかどうかをチェックする IsUnion<T> が作れる。
https://scrapbox.io/files/6528f80199128c001b6cb64e.png
面白い。