TypeScript

[TypeScript] 型推論がうまくいったりいかなかったりする話

TL, DR

型推論わからん

本題

こういうインターフェイスと関数があるとする

interface Named<T> {
    name: T;
}

function getName<T>(obj: Named<T>): T {
    return obj.name;
}

この関数をこう呼び出すと、Tはstringとして推論される。これは期待通り。

// nameの型はstringになる
const name = getName({ name: "John" });

では、Named<T> から派生した型を引数として取り、その型をそのまま戻り値に含めたい場合にはどうなるかというと、下のような形になるはず。

function getWithName<T, S extends Named<T>>(obj: S): { name: T, obj: S } {
    return { name: obj.name, obj };
}

これをこう呼び出した場合、期待としてはこの場合もTはstringとして推論されてほしい。

// ret は { name: string, obj: { name: string, age: number } } になってほしい
const ret = getWithName({ name: "John", age: 25 });

しかし、期待に反してTは {} として推論されてしまう。
{ name: string, age: 50 }{ name: string } からの派生であると同時に { name: {} } からの派生でもある訳なので、理屈としては理解できる。

それではということで、下のようにしてみると、期待通りに推論されるようになる。

// 引数の型を S から S & Named<T> に変更する
function getWithName2<T, S extends Named<T>>(obj: S & Named<T>): { name: T, obj: S } {
    return { name: obj.name, obj };
}
// ret2 は、期待通り { name: string, obj: { name: string, age: number } } と推論される
const ret2 = getWithName2({ name: "John", age: 25 });

理屈の通った、意図した挙動にも思えるし、たまたまそうなっているだけにも思える。
この挙動を今後も期待してよいものかどうか、それがわからない。

例えば、TypeScript 2.0のころは、 type Foo = "foo" | "bar" | string のようなUNION TYPEは、書いた通りのUNION TYPEとして扱われていたが、2.1で型の単純化処理が実装されたことによって、単に string として扱われるようになった。

同じように、 T2 extends T1 なら T2 & T1T2 と同値だよね、みたいな話になったらこの挙動は壊れるんじゃないかという気がする。

どうなんだろう。