小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。
前言
在TypeScript中,我们往往会定义定义一个联合类型,来解决一些业务中复杂数据的处理,如下
interface interfaceA {
name: string;
age: number;
}
interface interfaceB {
name: string;
phone: number;
}
const obj1 = { name: "andy", age: 2 };
const obj2 = { name: "andy", phone: 2 };
const arr1 = new Array<interfaceA>(50).fill(obj1);
const arr2 = new Array<interfaceB>(50).fill(obj2);
const arr3 = [...arr1, ...arr2];
const target = arr3[0];
console.log(target.name);
console.log(target.phone);
console.log(target.age);
复制代码
我们定义了一个接口interfaceA,一个接口interfaceB,他们各自对应一个具体的数据类型,但我们有时需要将这2个数据数组进行合并,TypeScript会自动推断这个数据的类型为
const arr3: (interfaceA | interfaceB)[]
复制代码
当我们通过数组操作取出数组里的元素时,这个元素的类型其实也是这个联合类型,但我们需要取值的时候,由于name属于interfaceA和interfaceB公用的,即交叉属性,TypeScript判定无风险,可用。但我们使用「phone」或者「age」这种不交叉的属性时候,TypeScript会报错,报错信息如下
类型“interfaceA | interfaceB”上不存在属性“phone”。
类型“interfaceA”上不存在属性“phone”。ts(2339)
复制代码
解决方案
- 把所有不交叉的属性都定义成可选属性,不好,因为对于interfaceB来说,可能age或者其他的属性就是必选的,「定义成可选属性」是一种逃避,且不安全
- 在每次用到的时候断言,比如我用到target.phone的时候,就(target as interfaceB).phone,不好,因为难道在一个作用域下,我每次取值都要断言,麻烦且有风险,
- 使用TypeScript中的自定义类型保护和类型谓词,我们需要创建一个函数,在这个函数的方法体中,我们不仅要检查
target
变量是否含有age
属性,而且还要告诉 TS 编译器,如果上述逻辑语句的返回结果是 true,那么当前判断的target
变量值的类型是 interfaceA 类型
创建一个自定义类型保护函数 —— isInterfaceA
,它的具体实现如下:
type interfaceAB = interfaceA | interfaceB;
const isInterfaceA = (item: interfaceAB): item is interfaceA => {
return (item as interfaceA).age !== undefined;
};
复制代码
你可以传递任何值给 isInterfaceA 函数,用来判断它是不是interfaceA。isInterfaceA函数与普通函数的最大区别是,该函数的返回类型是 item is interfaceA
,这就是我们前面所说的 “类型谓词”。
现在让我们来重构一下前面的条件语句:
if (isInterfaceA(target)) {
console.log(target.age); //target的类型为interfaceA
} else {
console.log(target.phone); //target的类型为interfaceB
}
复制代码
在重构完成后,我们再次运行代码,好了,现在问题已经解决了。
总结
自定义类型保护的主要特点是:
- 返回类型谓词,如
item is interfaceA
; - 包含可以准确确定给定变量类型的逻辑语句,如
(item as interfaceA).age !== undefined
。
对于基本数据类型来说,我们也可以自定义类型保护来保证类型安全,比如:
const isNumber = (variableToCheck: any): variableToCheck is number =>
(variableToCheck as number).toExponential !== undefined;
const isString = (variableToCheck: any): variableToCheck is string =>
(variableToCheck as string).toLowerCase !== undefined;
复制代码
如果你要检查的类型很多,那么为每种类型创建和维护唯一的类型保护可能会变得很繁琐。针对这个问题,我们可以利用 TypeScript 的另一个特性 —— 泛型,来解决复用问题:
function isOfType<T>(
varToBeChecked: unknown,
propertyToCheckFor: keyof T
): varToBeChecked is T {
return (varToBeChecked as T)[propertyToCheckFor] !== undefined;
}
复制代码
在以上代码中,我们定义了一个通用的类型保护函数,你可以在需要的时候使用它来缩窄类型。以前面自定义类型保护的例子来说,我们就可以按照以下方式来使用 isOfType
通用的类型保护函数:
if (isOfType<interfaceA>(target, "age")) {
console.log(target.age);
} else {
console.log(target.phone);
}
复制代码
有了 isOfType
通用的类型保护函数之后,你不必再为每个要检查的类型编写唯一的类型保护函数。而且在实际的开发过程中,只要我们合理的使用类型保护函数,就可以让我们的代码在运行时能够保证类型安全。