最初どうしてコンパイル通らないのか納得いかなかった
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 }として扱おう」というデザインにするなら避けられない話ですね。