0xf

日記だよ

高階関数の型定義で戻り値に ReturnType<T> を使う際に as を避けられないことへの納得

最初どうしてコンパイル通らないのか納得いかなかった

function sample<T extends () => string>(f: T): ReturnType<T> {
    return f() // コンパイルエラーとなる
}

これがコンパイルエラーとなり、Type 'string' is not assignable to type 'ReturnType<T>' と言われてしまう。なんでよ。fTなんだから、f()ReturnType<T> なんじゃないの。違うの。

例えば以下のようなコードを考えたときに、

function sample<T extends () => { name: string, age: number }>(f: T): ReturnType<T> {
    const result = f()
    return result // コンパイルエラーとなる
}

resultnameage のフィールドを持つことが想定されます。実際、それらの値を持つ型として 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 }として扱おう」というデザインにするなら避けられない話ですね。