泛型数组中空对象为何破坏类型传递?如何解决?
obj2的类型变成了{}? 这是TypeScript 2.6.2中数组字面量类型推断的特性导致的,核心原因在于空对象字面量{}的类型语义以及TypeScript对数组元素“最通用类型”的推断逻辑:
{}类型的特殊含义:在TypeScript中,{}并不是单纯表示“空对象”——它代表所有非null和非undefined的值。因为JavaScript中所有原始类型(number、string、symbol等)都可以被包装成对象(比如(1).toString()),所以TypeScript允许将原始类型赋值给{}类型,{}是一个覆盖范围极广的顶级类型(在unknown引入之前)。数组的最通用类型推断:当TypeScript推断数组字面量的类型时,会尝试找出所有元素的共同超类型。对于
[1, 'a', Symbol('sym'), {}]这个数组:1(number)、'a'(string)、Symbol('sym')(symbol)都可以赋值给{}类型- 空对象字面量
{}本身就是{}类型
所以TypeScript会把整个数组的类型推断为{}[],你的sample函数的泛型参数T就被推导为{},最终obj2的类型就是{}。
而obj3中的对象是{a: 'a'},它被推断为具体的{a: string}类型——这个类型和number、string、symbol没有共同的超类型(TypeScript不会优先选择any),所以TypeScript只能推断出这些类型的联合,也就是number | string | symbol | {a: string},符合你的预期。
根据你的需求,有几种方式可以让TypeScript正确推断出联合类型:
1. 显式指定泛型参数
调用sample时手动声明泛型T的类型,强制TypeScript使用联合类型:
const obj2 = sample<number | string | symbol | {}>([1, 'a', Symbol('sym'), {}]); // obj2 的类型:number | string | symbol | {}
2. 给空对象添加类型注解
把空对象字面量标注为更具体的“空对象”类型(比如Record<string, never>,表示没有任何属性的对象),这样TypeScript无法将原始类型赋值给它,只能推断联合类型:
const obj2 = sample([1, 'a', Symbol('sym'), {} as Record<string, never>]); // obj2 的类型:number | string | symbol | Record<string, never>
3. 升级TypeScript版本并使用const断言(推荐)
如果你能升级到TypeScript 3.4及以上,可以使用const断言让TypeScript将数组推断为元组,保留每个元素的精确类型:
const obj2 = sample([1, 'a', Symbol('sym'), {}] as const); // obj2 的类型:number | string | symbol | {}
4. 修改sample函数的泛型定义
调整函数的泛型参数,让它基于元组推断元素的联合类型(在2.6.2中可用):
function sample<T extends any[]>(array: T): T[number] { const index = Math.floor(Math.random() * array.length); return array[index]; } const obj2 = sample([1, 'a', Symbol('sym'), {}]); // obj2 的类型:number | string | symbol | {}
这个版本的sample会把输入数组当作元组处理,T[number]就是元组所有元素类型的联合,自然会保留所有元素的类型信息。
内容的提问来源于stack exchange,提问作者Simeon Vincent

