类型保护

类型保护是指缩小类型的范围,在一定的块级作用域内由编译器推导其类型,提示并规避不合法的操作,提高代码质量。
类型保护就是一些表达式,它们会在运行时检查以确保在某个作用域里的类型。
我们可以通过typeofinstanceofinis字面量类型将代码分割成范围更小的代码块,在这一块中,变量的类型是确定的。

typeof

先来看看JavaScript中typeof的用法:
具体可参考 MDN typeof

typeof 操作符返回一个字符串,表示未经计算的操作数的类型。
类型结果
Undefined"undefined"
Null"object"
Boolean"boolean"
Number"number"
BigInt(ECMAScript 2020 新增)"bigint"
String"string"
Symbol (ECMAScript 2015 新增)"symbol"
宿主对象(由 JS 环境提供)取决于具体实现
Function 对象"function"
其他任何对象"object"
// Undefined
typeof undefined === 'undefined';
typeof declaredButUndefinedVariable === 'undefined';
typeof undeclaredVariable === 'undefined';

typeof null === 'object';

typeof false === 'boolean';
typeof Boolean(1) === 'boolean'; // Boolean() 会基于参数是真值还是虚值进行转换

// Symbols
typeof Symbol() === 'symbol';
typeof Symbol('foo') === 'symbol';
typeof Symbol.iterator === 'symbol';

typeof 18n === 'bigint';

// 字符串
typeof 'hello' === 'string';
typeof String(1) === 'string';

// 数值
typeof 99 === 'number';
typeof NaN === 'number';
typeof Number(1) === 'number';  // Number 会尝试把参数解析成数值

// 对象 
typeof {a: 1} === 'object';

// 使用 Array.isArray 或者 Object.prototype.toString.call
// 区分数组和普通对象
typeof [1, 2, 4] === 'object';

typeof new Date() === 'object';
typeof /regex/ === 'object';


// 函数
typeof function() {} === 'function';
typeof class C {} === 'function'
typeof Math.sin === 'function';

TypeScript中的typeof主要用途是在类型上下文中获取变量或者属性的类型。
如:

  1. 获取变量类型

    function fn (x: string | number) {
      if (typeof x === 'string') {
     x.toFixed(2);       // Property 'toFixed' does not exist on type 'string'.
     return x.split('');  
      }  
      // ...
    }
  2. 获取对象的类型

    interface IPerson {
      name: string;
      age: number;  
    }
    let person: IPerson = {
      name: 'xman',
      age: 18  
    };
    type Person = typeof person;
    
    let p: Person = {
      name: 'zxx',
      age: 20  
    }

    以上代码中通过typeof获取到person对象的类型,之后我们就可以使用Person类型。
    对于嵌套对象也是一样:

    const userInfo = {
      name: 'xman',
      age: 18,
      address: {
     provice: '湖北',
     city: '武汉'
      }  
    }
    
    type UserInfo = typeof userInfo;

    此时UserInfo类型如下:

    type UserInfo = {
      name: string;
      age: number;
      address: {
       provice: string;
       city: string;
      };
    }  
  3. 获取函数的类型

    function add (x: number, y: number): number {
      return x + y;  
    }
    type Add = typeof add;

    此时Add类型为

    type Add = (x: number, y: number) => number

instanceof

先来看看JavaScript中instanceof的用法:
具体可参考 MDN instanceof

instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。
简单例子如下:
// 定义构造函数
function C(){}
function D(){}

var o = new C();


o instanceof C; // true,因为 Object.getPrototypeOf(o) === C.prototype


o instanceof D; // false,因为 D.prototype 不在 o 的原型链上

o instanceof Object; // true,因为 Object.prototype.isPrototypeOf(o) 返回 true
C.prototype instanceof Object // true,同上

C.prototype = {};
var o2 = new C();

o2 instanceof C; // true

o instanceof C; // false,C.prototype 指向了一个空对象,这个空对象不在 o 的原型链上。

D.prototype = new C(); // 继承
var o3 = new D();
o3 instanceof D; // true
o3 instanceof C; // true 因为 C.prototype 现在在 o3 的原型链上

TypeScript中instanceoftypeof类似,区别在于typeof判断基础类型,instanceof判断是否为某个对象的实例。
其右侧要求是一个构造函数.

class Person {
  public name: string;
  public age: number;  
  public constructor(theName: string, age: number) { 
    this.name = theName; 
    this.age = age;
  }
}

class Animal {
  public height: string;  
  public weight: string;
  public constructor(height: string, weight: string) { 
    this.height = height; 
    this.weight = weight;
  }
}

function typeGuard (arg: Person | Animal) {
  if (arg instanceof Person) {
    arg.height = '60kg';    // Property 'height' does not exist on type 'Person'.
  } else if (arg instanceof Animal) {
    arg.name = '猴子';      // Property 'name' does not exist on type 'Animal'.
  }
}

in

先来看看JavaScript中in的用法:
具体可参考 MDN in

如果指定的属性在指定的对象或其原型链中,则in 运算符返回true

注意: in右操作数必须是一个对象值。
简单例子如下:

const car = { make: 'Honda', model: 'Accord', year: 1998 };

console.log('make' in car);     // true;

delete car.make;
if ('make' in car === false) {
  car.make = 'Suzuki';
}

console.log(car.make);      // Suzuki


// 数组
var trees = new Array("redwood", "bay", "cedar", "oak", "maple");
0 in trees;        // 返回 true
3 in trees;       // 返回 true
6 in trees;        // 返回 false
"bay" in trees;    // 返回 false (必须使用索引号,而不是数组元素的值)

// 只是将一个属性的值赋值为undefined,而没有删除它,则 in 运算仍然会返回true
trees[3] = undefined;
3 in trees; // 返回 true

"length" in trees; // 返回 true (length 是一个数组属性)

Symbol.iterator in trees; // 返回 true (数组可迭代,只在 ES2015+ 上有效)


// 内置对象
"PI" in Math;          // 返回 true


var color1 = new String("green");
"length" in color1;  // 返回 true
var color2 = "coral";
"length" in color2;  // 报错 (color2 不是对象)

// 如果一个属性是从原型链上继承来的,in 运算符也会返回 true
"toString" in {}; // 返回 true

TypeScript中in操作符用于确定属性是否存在于某个对象上, 这也是一种缩小范围的类型保护

class Person {
  public name: string;
  public age: number;  
  public constructor(theName: string, age: number) { 
    this.name = theName; 
    this.age = age;
  }
}

class Animal {
  public height: string;  
  public weight: string;
  public constructor(height: string, weight: string) { 
    this.height = height; 
    this.weight = weight;
  }
}

function typeGuard (arg: Person | Animal) {
  if ('name' in arg) {
    arg.name = 'xman';     
  } else if ('height' in Animal) {
    arg.height = '100kg'; 
  }
}

类型谓词(is 关键字 )

类型谓词(type predicates): 为 parameterName is Type 这种形式。 parameterName必须来自于当前函数签名里的一个参数名。

is 关键字一般用于函数返回值类型中,判断参数是否属于某一类型,并根据结果返回对应的布尔类型。
定义一个类型保护,只要简单地定义一个函数,其返回值是一个 类型谓词
class Fish {
  swim () {
    console.log('游泳~');
  }
  eat () {
    console.log('进食!');
  }
}

class Bird {
  fly () {
    console.log('飞翔~');
  }
  eat () {
    console.log('进食!');
  }
}

function getSmallPet(): Fish | Bird {
  return Math.random() > 0.5 ? new Fish() : new Bird()
}
let pet = getSmallPet();

pet.eat();
pet.swim();
// Property 'swim' does not exist on type 'Fish | Bird'.
// Property 'swim' does not exist on type 'Bird'.
pet.fly();
// Property 'fly' does not exist on type 'Fish | Bird'.
// Property 'fly' does not exist on type 'Fish'.

以上代码中, getSmallPet函数中,即可以返回Fish类型对象,又可以返回Bird类型对象,由于返回对象类型不确定,所以使用联合类型对象共有的方法时,一切正常,但是使用联合类型对象各自独有的方法时,ts 会报错。
此时我们可以使用自定义类型保护来解决这个问题。
如下:

function isFish(pet: Fish | Bird): pet is Fish {
  return (pet as Fish).swim !== undefined;
}

if (isFish(pet)) {
    pet.swim();
} else {
    pet.fly();
}

拓展函数

const isNumber = (val: unknown): val is number => typeof val === 'number';
const isString = (val: unknown): val is string => typeof val === 'string';
const isSymbol = (val: unknown): val is symbol => typeof val === 'symbol';
const isFunction = (val: unknown): val is Function => typeof val === 'function';
const isObject = (val: unknown): val is Record<any, any> => val !== null && typeof val === 'object';

function isPromise<T = any>(val: unknown): val is Promise<T> {
  return isObject(val) && isFunction(val.then) && isFunction(val.catch);
}

const objectToString = Object.prototype.toString;
const toTypeString = (value: unknown): string => objectToString.call(value);
const isPlainObject = (val: unknown): val is object => toTypeString(val) === '[object Object]';

字面量类型保护

以下代码定义了两个接口和一个类别类型

interface Circle {
  kind: "circle";  // 字符串字面量类型
  radius: number;
}
 
interface Square {
  kind: "square";   // 字符串字面量类型
  sideLength: number;
}

type Shape = Circle | Square;

现在我们实现一个获取面积的方法:

function getArea(shape: Shape) {
  return Math.PI * shape.radius ** 2;   
  // Property 'radius' does not exist on type 'Shape'.
  // Property 'radius' does not exist on type 'Square'.
}

此时提示Square中不存在属性radius, 通过判断字面量类型来进行区分:

function getArea (shape: Shape) {
  switch (shape.kind) {
    case "circle":  // Circle类型
      return Math.PI * shape.radius ** 2;
    case "square":  // Square类型
      return shape.sideLength ** 2;
  }
}

最后考虑default,可以利用never类型的特性实现全面性检查。

function getArea (shape: Shape) {
  switch (shape.kind) {
    case "circle":  // Circle类型
      return Math.PI * shape.radius ** 2;
    case "square":  // Circle类型
      return shape.sideLength ** 2;
    default:    
      const _exhaustiveCheck: never = shape;
      return _exhaustiveCheck;
  }
}

注意: never类型表示的是那些永不存在的值的类型。
但是如果又新增了联合类型, 但是忘记同时修改switch case分支控制流程, 最后shape就会被收窄为 Triangle 类型, 导致无法赋值给never类型,这时就会产生一个编译错误。
所以在使用 never类型时一定要避免出现新增了联合类型而没有对应的实现的情况。

interface Triangle {
  kind: "triangle";
  sideLength: number;
}
type Shape = Circle | Square | Triangle;

function getArea (shape: Shape) {
  switch (shape.kind) {
    case "circle":  // Circle类型
      return Math.PI * shape.radius ** 2;
    case "square":  // Circle类型
      return shape.sideLength ** 2;
    default:    
      const _exhaustiveCheck: never = shape;  // Type 'Triangle' is not assignable to type 'never'.
      return _exhaustiveCheck;
  }
}

以上ts代码均在 https://www.typescriptlang.or... 上运行过,版本为4.7.2。
最后, 如有错误,欢迎各位大佬指点!感谢!

参考资料

https://rangle.io/blog/how-to-use-typescript-type-guards/
https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates
https://juejin.cn/post/6974746024464089124#heading-4


周潇林
13 声望2 粉丝