一、联合类型

在 TypeScript 中,一个变量不会被限制为单一的类型。如果你希望一个变量的值,可以有多种类型,那么就可以使用 TypeScript 提供的联合类型。下面我们来举一个联合类型的例子:

let stringOrBoolean: string | boolean = "Semlinker"; //只能是字符串或boolean类型

interface Cat {
  numberOfLives: number;
}

interface Dog {
  isAGoodBoy: boolean;
}

let animal: Cat | Dog;    //只能是Cat或Dog类型

当我们使用联合类型时,我们必须尽量把当前值的类型收窄为当前值的实际类型,而类型保护就是实现类型收窄的一种手段。

二、类型保护

类型保护是可执行运行时检查的一种表达式,用于确保该类型在一定的范围内。换句话说,类型保护可以保证一个字符串是一个字符串,尽管它的值也可以是一个数值。类型保护与特性检测并不是完全不同,其主要思想是尝试检测属性、方法或原型,以确定如何处理值。目前主要有四种的方式来实现类型保护:

1. in 关键字

检测属性、方法

interface Admin {
  name: string;
  privileges: string[];
}

interface Employee {
  name: string;
  startDate: Date;
}

type UnknownEmployee = Employee | Admin;

function printEmployeeInformation(emp: UnknownEmployee) {
  console.log("Name: " + emp.name);  //共有方法,编译ok
  if ("privileges" in emp) {  //检查是否有privileges属性
    console.log("Privileges: " + emp.privileges);  //会报编译错误
  }
  if ("startDate" in emp) {  //检查是否有startDate属性
    console.log("Start Date: " + emp.startDate);//会报编译错误
  }
}

注意:虽然我们在调用emp的属性之前,先用in判断了是否存在该属性,但是通过’emp.startDate’仍然会报编译错误,当 TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型里`共有的属性或方法``

Property 'startDate' does not exist on type 'UnknownEmployee'.
Property 'startDate' does not exist on type 'Admin'.

我们可以用 emp['startDate']避免编译错误

2. typeof 关键字

判断变量类型

typeof运算符的返回类型为字符串,值包括如下几种:

  • ‘undefined’ 未定义的变量或值
  • ‘boolean’ --布尔类型的变量或值
  • ‘string’ --字符串类型的变量或值
  • ‘number’ --数字类型的变量或值
  • ‘object’ --对象类型的变量或值,或者null(这个是js历史遗留问题,将null作为object类型处理)
  • ‘function’ --函数类型的变量或值
  • ‘Symbol’ --ES6中引入了一种新的基础数据类型
function padLeft(value: string, padding: string | number) {
  if (typeof padding === "number") {
      return Array(padding + 1).join(" ") + value;
  }
  if (typeof padding === "string") {
      return padding + value;
  }
  throw new Error(`Expected string or number, got '${padding}'.`);
}

typeof 类型保护只支持两种形式:typeof v === "typename"typeof v !== "typename""typename" 必须是"number""string""boolean""symbol"等。 但是 TypeScript 并不会阻止你与其它字符串比较,语言不会把那些表达式识别为类型保护。

3. instanceof 关键字

判断对象是否为某个类型的实例

interface Padder {
  getPaddingString(): string;
}

class SpaceRepeatingPadder implements Padder {
  constructor(private numSpaces: number) {}
  getPaddingString() {
    return Array(this.numSpaces + 1).join(" ");
  }
}

class StringPadder implements Padder {
  constructor(private value: string) {}
  getPaddingString() {
    return this.value;
  }
}

let padder: Padder = new SpaceRepeatingPadder(6);

if (padder instanceof SpaceRepeatingPadder) {
  // padder的类型收窄为 'SpaceRepeatingPadder'
}

4. 自定义类型保护的类型谓词(type predicate)

function isNumber(x: any): x is number {
  return typeof x === "number";
}

function isString(x: any): x is string {
  return typeof x === "string";
}

相信除了类型谓词外,大家对其它三种方式都很熟悉了,下面我们来着重介绍一下类型谓词。

三、类型谓词

在开始介绍类型谓词前,我们先来看一个示例:

interface Vehicle {
  move: (distance: number) => void;  //父类(接口)仅有move方法
}

class Car implements Vehicle {
  move = (distance: number) => {
    // Move car…
  };

  turnSteeringWheel = (direction: string) => {    //子类独有的turnSteeringWheel 
    // Turn wheel…
  };
}

class VehicleController {
  vehicle: Vehicle;
  constructor(vehicle: Vehicle) {
    this.vehicle = vehicle;
  }
}

const car = new Car();
const vehicleController = new VehicleController(car);

const { vehicle } = vehicleController;
vehicle.turnSteeringWheel('left');  //编译错误,父类没有turnSteeringWheel方法

const { vehicle } = vehicleController;用法可以参考《const定义变量时的{}》

尽管你知道汽车是一辆车,但 VehicleController 已经把它简化为一辆基本的汽车。因为 Vehicle并没有turnSteeringWheel属性,所以对于以上代码,TypeScript 编译器会提示以下错误信息:

Property 'turnSteeringWheel' does not exist on type 'Vehicle'.

对于这个问题,我们可以利用 instanceof 关键字来确保当前的对象是 Car 汽车类的实例:

if(vehicle instanceof Car) {
   vehicle.turnSteeringWheel('left');
}

但该方案有一定的限制,即它只对带继承关系的类有效。当判断的对象不是某个类的实例时就无效了,比如:

我们希望anotherCar也是一辆车

const anotherCar = {           //anotherCar也有车的特征,但是没有implements Car,不是Car的子类
  move: (distance: number) => null,                  //也有move方法
  turnSteeringWheel: (direction: string) => null   //也有turnSteeringWheel方法
}; 

const anotherVehicleController = new VehicleController(anotherCar);
const { vehicle } = anotherVehicleController;

if (vehicle instanceof Car) {      //返回false
  vehicle.turnSteeringWheel('left');
  console.log("这是一辆车");
} else {
  console.log("这不是一辆车");
}

尽管 anotherCar跟前面已经定义的 car 拥有相同的形状,但它并不是 Car汽车类的实例,因此在这种情况下,vehicle instanceof Car表达式返回的结果为false。所以以上代码的输出结果是:"这不是一辆车"。

尽管typeofinstanceof这两个关键字在很多情况下可以满足类型保护的需求,但在函数式编程的领域它们的功能就受限了。那么我们应该如何检查任何对象的类型的?幸运的是,你可以创建自定义类型保护。

3.1 自定义类型保护

下面我们继续以车辆和汽车的例子为例,来创建一个自定义类型保护函数 —— isCar,它的具体实现如下:

function isCar(vehicle: any): vehicle is Car {
  return (vehicle as Car).turnSteeringWheel !== undefined;
}

你可以传递任何值给 isCar 函数,用来判断它是不是一辆车。isCar 函数与普通函数的最大区别是,该函数的返回类型是vehicle is Car,这就是我们前面所说的 “类型谓词”。

isCar函数的方法体中,我们不仅要检查 vehicle 变量是否含有turnSteeringWheel属性,而且还要告诉 TS 编译器,如果上述逻辑语句的返回结果是 true,那么当前判断的 vehicle 变量值的类型是Car类型。

现在让我们来重构一下前面的条件语句:

// vehicle instanceof Car -> isCar(anotherCar)
if (isCar(anotherCar)) {     //返回true
  anotherCar.turnSteeringWheel('left');
  console.log("这是一辆车");
} else {
  console.log("这不是一辆车");
}

在重构完成后,我们再次运行代码,这时控制台会输出 “这是一辆车”。好了,现在问题已经解决了。接下来让我们来总结一下自定义类型保护有什么用?

3.2 自定义类型保护有什么用

自定义类型保护的主要特点是:

返回类型谓词,如 vehicle is Car
包含可以准确确定给定变量类型的逻辑语句,如 (vehicle as Car).turnSteeringWheel !== 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;

等价于

function isNumber (variableToCheck: any): variableToCheck is number {
    return (variableToCheck as number).toExponential !== undefined;
}

function isString (variableToCheck: any): variableToCheck is string {
    return (variableToCheck as string).toLowerCase !== undefined;
}

如果你要检查的类型很多,那么为每种类型创建和维护唯一的类型保护可能会变得很繁琐。针对这个问题,我们可以利用 TypeScript 的另一个特性 —— 泛型,来解决复用问题:

function isOfType<T>(
  varToBeChecked: any,
  propertyToCheckFor: keyof T
): varToBeChecked is T {
  return (varToBeChecked as T)[propertyToCheckFor] !== undefined;
}

keyof 可以参考 《TypeScript 泛型及应用》,作用是列出所有的属性

在以上代码中,我们定义了一个通用的类型保护函数,你可以在需要的时候使用它来缩窄类型。以前面自定义类型保护的例子来说,我们就可以按照以下方式来使用 isOfType通用的类型保护函数:

// isCar(anotherCar) -> isOfType<Car>(vehicle, 'turnSteeringWheel')
if (isOfType<Car>(vehicle, 'turnSteeringWheel')) {
  anotherCar.turnSteeringWheel('left');
  console.log("这是一辆车");
} else {
  console.log("这不是一辆车");
}

有了isOfType 通用的类型保护函数之后,你不必再为每个要检查的类型编写唯一的类型保护函数。而且在实际的开发过程中,只要我们合理的使用类型保护函数,就可以让我们的代码在运行时能够保证类型安全。