はじめに
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;
でも有効な結果が返ってきます。