typeof操作符与泛型的异常表现探究
在以下TypeScript代码中,函数fn里的o1和o2都是调用对应类型的函数得到的结果,但类型推导却一个是Returns[T],另一个是FooData | BarData | BazData,具体代码如下:
type MyUnion = 'foo' | 'bar' | 'baz'; type GenericData = { whatever: 'whatever' }; type FooData = { foo: 'foo' }; type BarData = { bar: 'bar' }; type BazData = { baz: 'baz' }; type MyData = { foo: FooData; bar: BarData; baz: BazData; } const builders = { foo: (_data: GenericData) => ({} as FooData), bar: (_data: GenericData) => ({} as BarData), baz: (_data: GenericData) => ({} as BazData), }; type Returns = { [Prop in keyof typeof builders]: ReturnType<typeof builders[Prop]>; }; type Builders = { [Prop in MyUnion]: (_data: GenericData) => Returns[Prop]; }; type Builders2 = { [Prop in MyUnion]: (typeof builders)[Prop]; }; declare const d: GenericData; function fn<T extends MyUnion>() { const b1 = {} as Builders[T]; const b2 = {} as Builders2[T]; const o1 = b1(d); // ^? const o1: Returns[T] const o2 = b2(d); // ^? const o2: FooData | BarData | BazData }
核心差异:映射类型中类型关联的保持方式
1. Builders的类型定义保留了Prop与返回值的强关联
Builders是一个映射类型,它为MyUnion中的每个Prop显式定义了函数类型:(_data: GenericData) => Returns[Prop]。这里的Returns[Prop]和当前遍历的Prop是直接绑定的——当用泛型T索引Builders[T]时,TypeScript会完整保留这种“T对应的返回值就是Returns[T]”的关联关系。因此调用b1(d)时,返回类型会被推导为和T绑定的Returns[T],不会直接展开成联合类型。
2. Builders2的类型定义丢失了Prop与返回值的关联
Builders2的定义是{ [Prop in MyUnion]: (typeof builders)[Prop] },typeof builders的实际类型是包含三个具体函数的对象:
{ foo: (_data: GenericData) => FooData; bar: (_data: GenericData) => BarData; baz: (_data: GenericData) => BazData; }
当用泛型T索引Builders2[T]时,TypeScript会将其解析为三个函数类型的联合:(typeof builders)['foo'] | (typeof builders)['bar'] | (typeof builders)['baz']。
根据TypeScript的规则,调用函数类型的联合时,返回值会是所有函数返回值的联合类型——因为TypeScript无法确定你调用的是联合中的哪一个具体函数,只能将返回值推导为所有可能的返回值集合,也就是FooData | BarData | BazData。
总结
Builders[T]是单一函数类型,返回值与T强绑定,调用后得到Returns[T]。Builders2[T]是多个函数类型的联合,调用联合函数时返回所有可能的返回值联合,因此得到FooData | BarData | BazData。
内容的提问来源于stack exchange,提问作者imshodan

