はじめに
ジェネリクスと Conditional Types で、アロー関数の戻り値の型を、より厳密に推論されるようにする方法を紹介します。
具体的には次のようなケースです。
この関数の使いみちはさておき、引数にboolean
を受け取り、'0'
もしくは 0
を返す関数です。
この関数は、呼び出し側としては、true
を与えた場合は'0'
を、false
の場合は0
という型推論を期待しますが、
実際にはどちらの場合でも'0'
または 0
と推論されてしまいます。
この動作は、戻り値の型注釈を省略していることが原因ではありません。
こういった場面には Conditional Types で、型情報を補足してあげると、より正確な型情報の導出が可能です。 では見ていきましょう。
型定義と Conditional Types
先程の例は、Conditional Types で次のように書き換えられます。
最初の<T extends boolean>
でジェネリクスT
を定義しています。T
はextends boolean
を満たす型すなわち、
boolean
やany
やnever
型とみなされます。戻り値の型注釈ではT
がextends true
を満たせば'0'
、それ以外では0
としています。
これだけで良ければ楽なのですが、残念ながらそうは行きません。 戻り値の値にはアサーションによって、型の上書きが必要です。
1つ目の例は、戻り値の型注釈と同じ型にキャストする方法です。こうすればコンパイルは通りますが、長くなり可読性が損なわれます。
また2つ目は、戻り値のどちらかをany
でキャストする方法です。なるべくany
は見たくないものですね。
この他にも方法があることにはあります。 アロー関数ではなく、関数宣言ならオーバーロードをすることで、一応回避できます。
ただしこれには痛みも伴い、ESLint を使っている場合には、色々無効にしなければなりません。 なにより、アロー関数ではなく関数宣言でなければならないことも大きな痛手です。
残念ながら、これ以外の方法がないようなので、ある選択肢で最良を模索しましょう。 現状最も気分がいいのは以下の書き方です。
冗長なので、これを短く書く方法を模索します。 以下のように書くことができます。
type
で変数のように型エイリアスを定義できます。これにはジェネリクスも使うことができます。
またジェネリクスは型引数が省略された場合のデフォルトの型を指定できます。
これで繰り返し現れた型注釈がぐっと短くなり、可読性が向上したのではないでしょうか。
Conditional Types と Union Distribution
最後に、引数によってどのように型が決定されるか見てみましょう。
先程触れたように、boolean
と互換性のあるany
やnever
も引数に受け取れるので、その時の型がどのように判定されるのか確認しましょう。
引数にtrue
やfalse
を指定したときは期待通りですが、boolean
やany
、never
のときはなぜこのような型が判定されるのでしょうか。
答えは Union Distribution が関係しています。
Union Distribution はジェネリクスがユニオン型の場合に、ユニオン型の各構成要素に対して別々に Conditional Types を評価するというものです。
boolean
はtrue
とfalse
のユニオン型なので、次のように評価します。
(true extends false ? "0" : 0) | (false extends false ? "0" : 0 )
これは"0" : 0 | "0"
となり、結果として0 | "0"
が推論されます。
any
の場合、Conditional Types は両辺のユニオン型となります。 そのため、結果はboolean
と同じようになります。
never
の場合はですが、これはnever
型が 0 個のユニオン型なことが起因します。
Conditional Types の結果も無条件に 0 個のユニオン型、つまりnever
型と判定されます。
Edit this page on GitHub