在 TypeScript中如何实现类型保护?类型谓词了解一下

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

前言

在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)
复制代码

解决方案

  1. 把所有不交叉的属性都定义成可选属性,不好,因为对于interfaceB来说,可能age或者其他的属性就是必选的,「定义成可选属性」是一种逃避,且不安全
  2. 在每次用到的时候断言,比如我用到target.phone的时候,就(target as interfaceB).phone,不好,因为难道在一个作用域下,我每次取值都要断言,麻烦且有风险,
  3. 使用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 通用的类型保护函数之后,你不必再为每个要检查的类型编写唯一的类型保护函数。而且在实际的开发过程中,只要我们合理的使用类型保护函数,就可以让我们的代码在运行时能够保证类型安全。

Guess you like

Origin juejin.im/post/7041765370415284237