类型守卫-有效保障类型安全性

267 阅读3分钟

如何有效地保障类型的安全性, 如下代码(将字符串或字符串数组全部转换为大写形式)会提示参数"strOrArray"隐式具有"any"类型 ts(7006)

 function convertToUpperCase(strOrArray) {
    if(typeof strOrArray === 'string') {
        return strOrArray.toUpperCase()
    } else if (Array.isArray(strOrArray)) {
        return strOrArray.map(item => item.toUpperCase())
    }
}

上述警告信息时因为缺少了参数类型注解,我们可以通过添加string | string[]类型注解来解决掉vscode的错误提示,而if, else iftypeof,Array.isArray将类型返回缩小。这种将类型范围缩小的动作我们称之为类型守卫, 实际上,它还可以用来区分类型集合中的不同成员。

类型集合一般包括联合类型枚举类型

如何区分联合类型

常用的类型守卫包括switch, 字面量恒等, typeof,instanceof, in和自定义类型守卫。

switch

在联合类型成员或者成员属性可枚举的场景中使用,即联合类型为字面量值的集合。

const convert = (c: 'a' | 1) => {
    swich(c) {
        case 1:
            return c.toFixed();
        case 'a':
            return c.toLowerCase();
    }
}

字面量恒等

switch适用的场景往往也可以使用字面量恒等比较进行替换。

const convert = (c: 'a' | 1) => {
    if (c === 1) {
        return c.toFixed()
    } else if (c === 'a') {
        return c.toLowerCase()
    }
}

一般建议,如果可枚举的值和条件分支越多, 使用switch就会让代码逻辑更简介,更清晰

typeof

当联合类型的成员不可枚举,比如说是字符串,数字等原子类型组成的集合,是个时候就需要使用typeof。

instanceof

当联合类型的成员是类是, 使用instanceof来减小类型范围

class Dog {
    wang = 'wangwang'
}

class Cat {
    miao = 'miaomiao'
}

const getName = (anmial: Dog | Cat) => {
    if (animal instanceof Dog) {
        return animal.wang
    } else if (animal instanceof Cat) {
        return animal.miao
    }
}

in

当联合类型的成员包含接口类型(对象),并且接口之间的属性不同

interface Dog {
    wang: string
}

interface Cat {
    miao: string
}

{
    const getName = (animal: Dog | Cat) => {
        if(typeof animal.wang === 'string') {
            return animal.wang // ts2339
        } else if (animal.miao) { // ts2339
            return animal.miao
        }
    }
}

{
    // 使用in操作符
    const getName = (animal: Dog | Cat) => {
        if ('wang' in animal) {
            return animal.wang; // ok
        } else if ('miao' in animal) {
            return animal.miao; //ok
        }
    }
}

自定义类型守卫 is

const isDog = function(animal: Dog | Cat): animal is Dog {
    return 'wang' in animal
}

const getName = (animal: Dog | Cat) => {
    if (isDog(animal)) {
        return animal.wang
    }
}

如何区分枚举类型

枚举特性:

  1. 枚举和其他任何枚举,类型都不可比较, 除了数字枚举可以与数字类型比较除外;
  2. 数字枚举极其不稳定

最佳实践, 永远不要拿枚举和除了自身之外的任何枚举,类型进行比较

enum A {
    one,
    two
}

enum B {
    one,
    two
}

const cpWithNumber = (param: A) => {
    if (param === 1) { // bad
        return param
    }
    
    if (param === B.two as unknown as A) { // bad
        return param
    }
    
    if (param === A.two) { // good
        return param
    }
}

字面量成员枚举可等价为字面量成员类型组成的联合类型,所以类型守卫可以让字面量成员枚举发生类型缩小。

失效的类型守卫

ts4.3.5之前版本,某些场景下,比如下面的例子中,类型守卫会失效。

const getName = <T extends Dog | Cat>(animal: T) => {
    if('wang' in animal) {
        return animal.wang; //ts 2339
    }
    return animal.miao; // ts 2339
}

in并没有让类型缩小为Dog类型, 可以使用instanceof 来替换in操作符,来缩小类型范围。后面的代码可以使用(animal as Cat).miao

其他

什么时候使用双重断言

A不能直接断言成B, 就需要使用双重断言.

 const animal = new Dog()
     // 直接断言,会提示ts2352, 两种类型不能充分重叠。 如果是有意的,需要先转换成unknown
 animal as Cat
 animal as unknown as Cat