Typescript 可以根据字段存在(未定义/不存在)而不是值来保护/区分吗?

Mat*_*ead 1 undefined discriminated-union typescript type-narrowing union-types

谁能解释为什么 Typescript 可以使用in关键字缩小类型,但不能通过存在非未定义的值来缩小类型?我正在将一个大型代码库从 JS 移植到 TS,并且对该结构进行了非常广泛的使用if (x.something) { ... }

declare const x: { a?: object } | { b: number };

if ('a' in x) { 
  const Q = x.a;  // Q: object | undefined, correct but not very helpful - still have to test Q for non-undefined
}

if (x.a) { 
  const Q = x.a; // Doesn't work, but if it did, Q: object, which is helpful
}

if (typeof x.a !== "undefined") { 
  const Q = x.a; // Same as above
}
Run Code Online (Sandbox Code Playgroud)

请注意,如果它不是联合体,它将按预期工作:

declare const x: { a?: object }

if ('a' in x) { 
  const Q = x.a;  // Q: object | undefined, correct but not very helpful
}

if (x.a) { 
  const Q = x.a; // Q: object (yay!)
}
Run Code Online (Sandbox Code Playgroud)

Kar*_*ski 5

问题

\n\n

有一些规则需要记住:

\n\n
    \n
  • 您只能读取联合体每个成员上存在的属性。这就是为什么if (x.a)错误 \xe2\x80\x94a没有在联合的第二个成员上定义。
  • \n
  • 对象类型允许有多余的属性。TypeScript 使用结构类型。这意味着类型{ foo: string, bar: number }通常可以分配给 type { foo: string }。这就是为什么if (\'a\' in x)\xe2\x80\x94 任何具有名为 的属性的类型都a无法通过该检查的原因。但我们不能保证该值将是一个对象。假设它是不安全的。
  • \n
  • 为了分离联合,更喜欢用户定义的类型保护而不是内联检查,例如typeof x === "string". 类型保护有一个特殊的语法,告诉 TypeScript 缩小检查的类型。这就是为什么if (typeof x.a !== "undefined")在你的例子中不起作用的原因。
  • \n
\n\n

解决方案

\n\n

让您的工会独一无二。告诉 TypeScript 如果a存在,则b永远不会被定义,反之亦然。

\n\n
declare const x: { a?: object, b?: undefined } | { b: number, a?: undefined }\n
Run Code Online (Sandbox Code Playgroud)\n\n

请注意,我们将不需要的属性标记为可选。相反,如果我们这样做{ a?: object, b?: undefined } | { b: number, a?: undefined },那么将需要不需要的属性,并且x必须将它们显式设置为undefined

\n\n

您现在可以使用这些方法来处理x.

\n\n
function isDefined<T>(candidate: T | null | undefined): candidate is T {\n  return candidate != null;\n}\n\nif (x.a) { \n  const Q = x.a; // object\n}\n\nif (isDefined(x.a)) {\n  const Q = x.a; // object\n}\n\nif (typeof x.a !== "undefined") { \n  const Q = x.a; // object\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

请注意,您仍然无法使用使用运算符的方法in。这样做有一个很好的理由:它可以防止误报。只要我们不需要的属性的值显式设置为 ,就允许它们存在于对象上undefined。考虑以下示例:

\n\n
function test(x: { a?: object, b?: undefined } | { b: number, a?: undefined }): void {\n  if (\'a\' in x) {\n    x.a; // object | undefined (good). We cannot expect object here.\n  }\n}\n\ntest({ b: 1, a: undefined }); // "a" is not an object!\n
Run Code Online (Sandbox Code Playgroud)\n\n

提示:使用ExclusiveUnion助手

\n\n

?undefined我们可以创建一个助手来为我们执行此操作,而不是将不需要的属性标记为。

\n\n
declare const x: ExclusiveUnion<{ a?: object } | { b: number }>;\n
Run Code Online (Sandbox Code Playgroud)\n\n

执行:

\n\n
type DistributedKeyOf<T> =\n  T extends any\n    ? keyof T\n    : never;\n\ntype CreateExclusiveUnion<T, U = T> =\n  T extends any\n    ? T & Partial<Record<Exclude<DistributedKeyOf<U>, keyof T>, never>>\n    : never;\n\ntype ExclusiveUnion<T> = CreateExclusiveUnion<T>;\n
Run Code Online (Sandbox Code Playgroud)\n

  • 很好的答案 - 谢谢您的详细解释。我认为“多余属性”位是我忘记了为什么它不起作用的地方,并且创建具有公共字段的联合以供鉴别器检查的解决方案非常优雅,在我的情况下是理想的,因为它用于 TS 修改并且不涉及对现有JS的大量修改。选为最佳答案。谢谢! (2认同)