TypeScript细碎知识点:类型保护

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

typeof

⏰ typeof 操作符返回一个字符串,表示未经计算的操作数的类型。

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

  • 获取变量类型

    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类型

  • 获取对象的类型

    interface Iperson {
        name: string,
        age: number;
    }
    
    let person: Iperson = {
        name: 'xman',
        age: 18
    }
    
    type Person = typeof person;
    
    let p : Person = {
        name: 'ts',
        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;
      };
    }  
  • 获取函数的类型
    function add (x: number, y: number): number {
      return x + y;  
    }
    type Add = typeof add;

    此时Add类型为

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

instanceof

instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。

TypeScript中 instanceof 与 typeof 类似,区别在于 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

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

🔔 注意: in右操作数必须是一个对象值。

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();
}

常见的类型判断方法封装

  • 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]'
  • isSet: 判断是否为Set

    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;
  }
}

posted on 2024-07-03 16:30  梁飞宇  阅读(22)  评论(0)    收藏  举报