网站搜索

动态检查 TypeScript 对象是否具有某个键而不使用自定义类型保护


我可以使用 TypeScript 的 in 关键字,以类型安全的方式检查对象是否具有某个键,如果将该键写为字符串字面量:

function guardHasTest <Data extends object> (
  value: Data
): Data & Record<'test', unknown> {
  if (!('test' in value)) {
    throw new Error('Missing key')
  },
  return value
}

然而,如果我把键设为动态字符串,相同的语法不会缩小类型:

function guardHasKey <Data extends object, Key extends string> (
  value: Data,
  key: Key
): Data & Record<Key, unknown> {
  if (!(key in value)) {
    throw new Error('Missing key')
  }
  return value
  // Type 'Data' is not assignable to type 'Data & Record<Key, unknown>'.
  Type 'object' is not assignable to type 'Data & Record<Key, unknown>'.
    Type 'object' is not assignable to type 'Data'.
      'object' is assignable to the constraint of type 'Data', but 'Data' could be instantiated with a different subtype of constraint 'object'.
        Type 'Data' is not assignable to type 'Record<Key, unknown>'.
          Type 'object' is not assignable to type 'Record<Key, unknown>'.ts(2322)
}

我不想使用自定义类型保护,因为它们的逻辑不是类型安全的:

export function isKeyOf<T, const Key extends string>(
  obj: T,
  key: Key
): obj is T & Record<Key, unknown> {
  return true // No error, even though the logic is not correct
}

我如何能够以完全类型安全的方式让一个函数动态检查一个对象是否具有某个键?

答案#1

之所以 guardHasTest() 能正常工作,是因为它利用了对未列出属性缩小的支持,该功能通过 in 运算符实现,并在 microsoft/TypeScript#50666 中实现,随 TypeScript 4.9 发布。在 TypeScript 4.9 之前的版本中,这会给你与 guardHasKey() 中相同的错误。它是一个相对较新的功能。

用于 microsoft/TypeScript#50666 的 PR 是为了解决 microsoft/TypeScript#21732 的功能请求。它处理像 "test" 这样的字面量类型的键。它特别不处理泛型键的情况:

我将此 PR 标记为修复 #21732,尽管它并没有处理 key in obj 的情况,其中 key 是某种泛型类型。一般来说,当 key 是某种非有限类型(例如 string)时,我们唯一可以有意义地反映出 key in obj 检查后的效果是,obj[key] 应该是一个有效的表达式(类型为 unknown),前提是 key 和 obj 在检查后都已知未被修改。这可能可以在 if 语句或三元运算符的真分支中实现,但在所有控制流情况下都无法实现。

这就是官方给出的关于为什么没有实现的说法;认为对于非字面量键,在所有需要进行控制流分析的地方实现 in 缩小是不可能的。如果你希望这里有不同的情况,你可以为此提交一个功能请求,但我对其实现不抱太高的期待。


这意味着目前无法让编译器仅通过注意到 guardHasKey() 按预期工作来自动处理。 (实际上,想象一下 guardHasKey(obj, Math.random()<0.5 ? "a" : "b") 应该缩小到什么类型,有点有趣。它应该是 typeof obj & {a: unknown; b: unknown},就像 Record 所暗示的那样?还是应该是 typeof obj & ({a: unknown} | {b: unknown}),或者其他什么类型? 事实证明,这正是阻碍针对 microsoft/TypeScript#13948 提出的修复方案的类似难题。)

目前,你可以选择使用类型断言来强制 return 行编译,或者你可以编写一个自定义类型保护函数,比如在 microsoft/TypeScript#21732 中编写的那个。当然,这两者都不能保证编译器的类型安全。但这正是这些功能的意义所在,让你表达编译器不知道的东西。所以,尽管你可能出于某种类型纯粹性的理想而倾向于避免类型保护函数,但要知道 TypeScript 并不是为了 100% 完全通过编译器验证类型安全而设计的,而自定义类型保护函数正是让你将不安全部分(函数的实现)与安全部分(函数的调用者)分开的构建块之一。