TypeScript基础之类型保护

517 阅读8分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第9天,点击查看活动详情

类型保护

类型保护是指缩小类型的范围,在一定的块级作用域内由编译器推导其类型,提示并规避不合法的操作,提高代码质量。
类型保护就是一些表达式,它们会在运行时检查以确保在某个作用域里的类型。
我们可以通过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('');  
  }  
  // ...
}

以上代码中,在if条件判断中使用typeof 判断 变量x的类型是否是string类型

  1. 获取对象的类型
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;
  };
}  
  1. 获取函数的类型
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'.
  }
}

以上代码中, 通过instanceof判断变量arg是否为Person或者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'; 
  }
}

以上代码中, 通过in确定属性是否存在于Person或者Animal实例对象上,从而进行不同的操作。

类型谓词(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();
}

常见的类型判断方法封装

以下代码来自vue-next源码:

  • hasOwn: 判断val是否含有属性key
const hasOwnProperty = Object.prototype.hasOwnProperty
export const hasOwn = (
  val: object,
  key: string | symbol
): key is keyof typeof val => hasOwnProperty.call(val, key)
  • isMap: 判断是否为Map
export const isMap = (val: unknown): val is Map<any, any> =>
  toTypeString(val) === '[object Map]'
  • isMap: 判断是否为Map
export const isSet = (val: unknown): val is Set<any> =>
  toTypeString(val) === '[object Set]'
  • isDate: 判断是否为日期
export const isDate = (val: unknown): val is Date => val instanceof Date
  • isFunction: 判断是否为function
export const isFunction = (val: unknown): val is Function =>
  typeof val === 'function'
  • isString: 判断是否为string
export const isString = (val: unknown): val is string => typeof val === 'string'
  • isSymbol: 判断是否为symbol
export const isSymbol = (val: unknown): val is symbol => typeof val === 'symbol'
  • isObject: 判断是否为object且不是null
export const isObject = (val: unknown): val is Record<any, any> =>
  val !== null && typeof val === 'object'
  • isPromise: 判断是否为promise
export const isPromise = <T = any>(val: unknown): val is Promise<T> => {
  return isObject(val) && isFunction(val.then) && isFunction(val.catch)
}
  • isPlainObject: 判断是否为plainObject
export const objectToString = Object.prototype.toString

export const toTypeString = (value: unknown): string =>
  objectToString.call(value)

export 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代码均在 www.typescriptlang.org/play 上运行过,版本为4.7.2。
最后, 如有错误,欢迎各位大佬指点!感谢!

参考资料

rangle.io/blog/how-to…
www.typescriptlang.org/docs/handbo…
juejin.cn/post/697474…