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