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

Can Typescript guard / discriminate based on field presence (undefined / absent) rather than value?

谁能解释为什么 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
}

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

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!)
}

可以使用自定义类型检查吗? 例如:

export const instanceOfIImportNotification = (_o: any): _o is IImportNotification => {
  return 'metaData' in _o && 'importType' in _o.metaData && 'azureFilePath' in _o.metaData;
};

问题

有几条规则需要牢记:

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

解决方法

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

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

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

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

function isDefined<T>(candidate: T | null | undefined): candidate is T {
  return candidate != null;
}

if (x.a) { 
  const Q = x.a; // object
}

if (isDefined(x.a)) {
  const Q = x.a; // object
}

if (typeof x.a !== "undefined") { 
  const Q = x.a; // object
}

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

function test(x: { a?: object, b?: undefined } | { b: number, a?: undefined }): void {
  if ('a' in x) {
    x.a; // object | undefined (good). We cannot expect object here.
  }
}

test({ b: 1, a: undefined }); // "a" is not an object!

提示:使用ExclusiveUnion助手

我们可以创建一个助手来为我们做这件事,而不是将不需要的属性标记为 ?undefined

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

实施:

type DistributedKeyOf<T> =
  T extends any
    ? keyof T
    : never;

type CreateExclusiveUnion<T, U = T> =
  T extends any
    ? T & Partial<Record<Exclude<DistributedKeyOf<U>, keyof T>, never>>
    : never;

type ExclusiveUnion<T> = CreateExclusiveUnion<T>;