You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为何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类型中。

实际行为

代码可正常编译,未触发预期的类型错误。

问题原因分析

  1. 泛型约束的遗留影响:
    原始函数的泛型T被约束为AorB[],即使args被收窄为A[],T的底层约束依然保留了AorB的可能性(比如T可以是(A & B)[]这种交叉类型数组)。此时keyof T[number]仍包含"b",导致["b"]被判定为合法参数。
  2. 泛型参数绑定问题:
    调用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 02:50:15