为何TypeScript类型收窄后泛型函数仍允许无效键?
问题:TypeScript泛型函数未正确识别数组类型收窄后的键约束
问题背景
我有一个泛型函数haveSameValue,用于检查数组中所有对象的指定属性是否具有相同值:
function haveSameValue<T extends any[]>(objects: [...T], keys: (keyof T[number])[]): boolean { return objects.slice(1).every(obj => keys.every(key => obj[key] === objects[0][key]) ); }
该函数整体运行正常,但在类型收窄场景下出现意外问题:
interface A { a: string; } interface B { b: string; } type AorB = A | B; function isA(arg: AorB): arg is A { return "a" in arg; } function foo<T extends AorB[]>(args: [...T]) { if (args.every(isA)) { if (haveSameValue(args, ["b"])) { // 此处未触发编译错误,但"b"并非"A"的键 } } }
预期行为
经过args.every(isA)检查后,args应被收窄为A[],传入["b"]给haveSameValue时应触发编译错误,因为"b"不存在于A类型中。
实际行为
代码可正常编译,未触发预期的类型错误。
问题原因分析
- 泛型约束的遗留影响:
原始函数的泛型T被约束为AorB[],即使args被收窄为A[],T的底层约束依然保留了AorB的可能性(比如T可以是(A & B)[]这种交叉类型数组)。此时keyof T[number]仍包含"b",导致["b"]被判定为合法参数。 - 泛型参数绑定问题:
调用haveSameValue(args, ["b"])时,TypeScript会自动推断泛型参数为原始的T,而非收窄后的T & A[],因此不会触发基于A类型的键约束检查。
实验验证对比
- 直接传入
A[]时,泛型被推断为A[],keyof T[number]为"a",触发预期错误:haveSameValue([{ a: "a" }, { a: "a" }], ["b"]); // => Type '"b"' is not assignable to type '"a"'.ts(2322) - 显式指定收窄后的泛型类型时,
keyof ([...T] & A[])[number]等价于keyof (T[number] & A),仅包含"a",因此触发错误:function foo2<T extends AorB[]>(args: [...T]) { if (args.every(isA)) { if (haveSameValue<[...T] & A[]>(args, ["b"])) { // ... } } } // => Type '"b"' is not assignable to type '"a" | keyof T[number]'.ts(2322)
解决方案
修改haveSameValue的泛型绑定逻辑,让它直接基于数组元素类型而非数组本身的泛型类型进行约束,这样能更好地利用类型收窄的结果:
function haveSameValue<T>(objects: T[], keys: (keyof T)[]): boolean { return objects.slice(1).every(obj => keys.every(key => obj[key] === objects[0][key]) ); }
效果验证
修改后,在foo函数中,args.every(isA)会将args收窄为A[],此时T被推断为A,keys必须是(keyof A)[](即["a"]),传入["b"]会立即触发编译错误,符合预期。
保持灵活性的重载方案
如果需要保留原函数对元组类型的支持(比如[...T]可变参数场景),可以使用重载实现:
// 支持元组类型的重载 function haveSameValue<T extends readonly any[]>(objects: T, keys: (keyof T[number])[]): boolean; // 支持普通数组类型的重载,适配类型收窄 function haveSameValue<T>(objects: T[], keys: (keyof T)[]): boolean; // 实现逻辑 function haveSameValue(objects: any[], keys: PropertyKey[]): boolean { return objects.slice(1).every(obj => keys.every(key => obj[key] === objects[0][key]) ); }
该方案既支持元组场景的精确类型推断,又能在数组被收窄时正确约束键的类型。
内容的提问来源于stack exchange,提问作者Codemozart
相关产品推荐
相关产品推荐

