誰も書いてる人がいなかったので。執筆時点のTypeScriptのバージョンは4.2.3です。
以下の関数を見てください。
function f<T>(x: T extends string ? never : T) { return x }
このシグネチャが型レベルでどう動作するのか、ぱっと見でわかりますか? 初めて見たとき筆者はわかりませんでした。しかし、ちょっと考えてみればなんとなく予想はつくでしょう。この引数x
は、string
型を受け取ろうとしているときだけnever
型として振舞う(つまりstring
型だけを禁止する引数となる)という挙動をとります。このことの利点は、まだ真にTSに入ってはいないNegated Typesを(関数の引数の位置に限って)表現できる、ということももちろんですが、もっと重要なのは以下の点です。たとえば引数の代わりに返り値にConditional Typesを使用して:
function g<T>(x: T): T extends string ? never : T { return x as any }
このようにした場合、関数の呼び出し時にTSにエラーを吐かせることができません。その関数のnever
な返り値を実際に他のところで使おうとしたときに初めて型の異常が発覚します(しかもnever
型の値はどんな型の変数にも入れられるので、もしかしたら異常が発覚しないままかもしれません)。
f(42)
f("42") // Argument of type 'string' is not assignable to parameter of type 'never'.
g(42)
g("42") // NO ERROR HERE!!!!!!?!?!??!?!?!?!!?!!!!?!??!
これでは遅いと感じられる場合、引数の位置でConditional Typesを使うことを考えてください。
筆者がぱっと見でわからなかった原因は、型判断の結果がどう扱われるのかが、他の位置におけるそれと異なるからです。通常、A extends B ? C : D
のC
やD
がそのまま型変数への束縛になることはありません。しかし引数の位置では、驚くべきことにそれが可能なのです。しかもこの束縛は式の外側の型変数に対して遡及的に起こっているのが面白いところです。