はじめに
TypeScriptを使っているとnever同士比較をしたい時があります。しかし、neverは他の型と異なり少々工夫して比較する必要があります。この記事では型の比較を使用する一例として、引数の型がneverであることを確認する型を解説します。
解説
neverであることを確認するコードは下の通りです。
type IsNever<X> = X[] extends never[] ? true : false;
下のような他の型であることを確認のためのコードは動きません。
type IsNever<X> = X extends never ? true : false;
Conditional Types
まずConditional Typesの仕様を見ていきます。Confitional TypesはTypeScriptの型システムの中で条件型としての役割を担っています。例として下のようなコードを考えます。
type Compare<X> = X extends Y ? true : false;
このコードはXがYに割り当てることが可能な場合にtrueを不可能な場合にfalseを返すようになっています。このような比較して型を抽出することをConditional Typesと言います。条件型としての役割を持つだけあって確かにX extends Yを条件式とした場合三項演算子と似た形式ですね。
さて、このConditional Typesには面白い仕様があります。Xにユニオン型を渡した時の挙動を想像してください。例えば下のように型を受け取ってその型で配列を作成する型に、ユニオン型を引数として渡すことを考えます。
type ToArray<Type> = Type extends any ? Type[] : never
type TypeScript = ToArray<'type' | 'script'>
この時TypeScriptの型はどうなるでしょうか。これから説明する仕様を知らない場合('type' | 'script')[]となると考えられます。しかし、この結果は'type'[] | 'script'[]となります。これはXがユニオンの場合二つに分かれることに起因しています。つまり'type' | 'script' extends anyは'script' extends any | 'script' extends anyのように別れて評価されます。この仕様をDistributive Conditional Types
と言います。
never
次はneverについて考えていきます。neverがstringとユニオン型を取ることを考えてください。string | neverこれが使用されるときは使用者はstringとして扱います。つまりneverは空(ないもの)として扱われるわけです。そのためneverをユニオン型として扱おうとした時、空として扱われます。
結論
以上のConditional Typesの仕様とneverとユニオンの仕様を組み合わせて考えると、Xがneverだったときこの型は extends Yと分離されて、空との比較になってしまします。そのため、never extends never ? true : falseは extends never ? true : falseとなってfalseを返すようになってしまうわけです。
そこで、neverを直接比較せず、never[]や[never]のようにして比較することを考えます。neverをユニオンとして扱うとからですが、never[]や[never]をユニオンとして扱った場合は配列ですので空になりません。そのため最初に紹介した
type IsNever<X> = X[] extends never[] ? true : false;
が有効となります。察しの通り
type IsNever<X> = [X] extends [never] ? true : false;
でも有効な結果が返ってきます。