TypeScriptのジェネリクスをデフォルト引数と組み合わせようとしたら、型が合わない事態に陥ってしまいました。
事例
「何かしらの変換を行う機能はあるけど、省略すれば何も行わない」ような挙動をさせたい場所はちょくちょくあるかと思いますが、そのような状況でデフォルト引数を使ってハマってしまいました。
ここでは、1つの例としてprocessArray(arr, [func])
というような関数を考えてみます。arr
には配列を、func
には要素を変換する関数を必要なら渡す、Array.from
の劣化コピーのような関数です。
デフォルト引数で失敗
ここで、実装の手間を省くために、引数の関数が渡されないときは恒等関数(x => x
)で処理させることにしたとします。デフォルト引数にこれを入れてTypeScriptにしてみます。
const identity = <T,>(x: T) => x;
const processArray = <T, U>(arr: readonly T[], func: (val: T) => U = identity): U[] => {
return arr.map(func);
}
これはエラーになってしまいます(Type '(x: T) => T' is not assignable to type '(val: T) => U'.
Type 'T' is not assignable to type 'U'.
'U' could be instantiated with an arbitrary type which could be unrelated to 'T'.)。tsc曰く、
Type '<T>(x: T) => T' is not assignable to type '(val: T) => U'.
Type 'T' is not assignable to type 'U'.
'U' could be instantiated with an arbitrary type which could be unrelated to 'T'.
とのことで、デフォルト引数が適用された場合についてU
の型を制御する、というのはできないようです。
省略可能な引数にしてみる
一方で、func
を省略可能な引数で書く方法も考えられます。
const identity = <T,>(x: T) => x;
const processArray = <T, U = T>(arr: readonly T[], func?: (val: T) => U): U[] => {
return arr.map((func || identity) as (val: T) => U);
}
こちらは成功します。
結論
ジェネリックな引数にデフォルト引数を設定しようとすると、うまく型が付かない。