最初どうしてコンパイル通らないのか納得いかなかった
function sample<T extends () => string>(f: T): ReturnType<T> { return f() // コンパイルエラーとなる }
これがコンパイルエラーとなり、Type 'string' is not assignable to type 'ReturnType<T>'
と言われてしまう。なんでよ。f
はT
なんだから、f()
は ReturnType<T>
なんじゃないの。違うの。
例えば以下のようなコードを考えたときに、
function sample<T extends () => { name: string, age: number }>(f: T): ReturnType<T> { const result = f() return result // コンパイルエラーとなる }
result
は name
と age
のフィールドを持つことが想定されます。実際、それらの値を持つ型として f()
の結果は推論されます。
ところで、
sample(() => ({ name: "勇者", age: 16, hp: 10 }))
こんな呼び出され方をした場合、この戻り値の型は { name: string, age: number, hp: number }
となるでしょう
これは { name: string, age: number }
のサブタイプです。なるほど。
なので ReturnType<T>
に f()
の結果を当てはめようとするとダウンキャストが必要であり、暗黙の変換はされずエラーとなるのは自然です。
ということで
function sample<T extends () => { name: string, age: number }>(f: T): ReturnType<T> { return f() as ReturnType<T> }
この as は避けられないという納得が得られた。これは 「T
は () => { name: string, age: number }
なんだから戻り値は { name: string, age: number }
として扱おう」というデザインにするなら避けられない話ですね。