はじめに
ジェネリクスと 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
