乐趣区

关于typescript:TypeScript基础之类型保护

类型爱护

类型爱护是指放大类型的范畴,在肯定的块级作用域内由编译器推导其类型,提醒并躲避不非法的操作,进步代码品质。
类型爱护就是一些表达式,它们会在运行时查看以确保在某个作用域里的类型。
咱们能够通过 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

退出移动版