TypeScript中的一些实用技能:类型断言、多态、类型守卫、抽象类

638 阅读6分钟

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

1. 类型断言

  1. 啥是类型断言? 把两种有重叠关系的数据类型进行相互转换的一种TS语法
  2. 格式
    • A数据类型的变量 as B数据类型
    • <B>A A数据类型和B数据类型必须具有重叠关系
  3. 重叠关系场景:
    • A、B是类,且存在继承关系。他们之间类型可以相互转换,但大多数情况下是把父类的实例对象断言成子类类型。
        class Preson {
          .....
        }
      
        class Student extends Preson {
          ....
        }
      
        let stu = new Student() as Preson; // 正确
        let Pre = new Preson() as Student; // 正确
      
    • A、B是类,但不存在继承关系。 类A中的所有public修饰的属性和方法的集合与类B中的所有public修饰的属性和方法的集合一致或是其子集,则A、B两个类之间的类型可以相互转换。
        class Preson {
          public name: string;
          public sex: string;dxzxjhggggggg
        }
      
        class Student{
          public name: string;
          public sex: string;
          public no: number;
        }
      
        let stu = new Student() as Preson; // 正确
        let Pre = new Preson() as Student; // 正确
      
    • A为类,B为interface/type,且A类实现了B接口 可以相互断言成彼此的类型
      export interface Subject {
        a: string;
        b: number;
      }
      
      export class SubjectClass implements Subject {
        a: string;
        b: number;
        c: string
        constructor(a: string, b: number, c: string) {
          this.a = a;
          this.b = b;
          this.c = c;
        }
      }
      
      let sub = new SubjectClass('1', 2, 'c') as Subject // 正确
      let interfaceSub: Subject = {
        a: '',
        b: 3
      };
      
      (interfaceSub as SubjectClass).c // 正确
      
    • A为类,B为interface/type,但A类没有实现B接口,同上述第2项
      export interface Subject {
        a: string;
        b: number;
      }
      
      export class SubjectClass {
        a: string;
        b: number;
        c: string
        constructor(a: string, b: number, c: string) {
          this.a = a;
          this.b = b;
          this.c = c;
        }
      }
      
      let sub = new SubjectClass('1', 2, 'c') as Subject // 正确
      let interfaceSub: Subject = {
        a: '',
        b: 3
      };
      
      (interfaceSub as SubjectClass).c // 正确
      
    • A的类型为联合类型
      function getId(key: number | string) {
        key as number
      }
      
    • 任何数据类型都可以转换成anyunknown类型,同时anyunknown类型也可以转换成其他任何类型

2. TS类型守卫

  1. new干了啥?

    newObj(constru, ...args) {
      // 先定义一个空对象
      let obj = {};
    
      // 链接到原型
      obj.__proto__ = constru.prototype;
    
      // 执行构造函数,并把this指向前面定义的空对象
      let result = constru.apply(obj, args)
      
      // 确保 new 出来的是个对象
      return typeof result === 'object' ? result : obj
    }
    
  2. 啥是类型守卫? 在块级作用域【if或条目运算符】内,通过对类型的判断,从而缩小TS对变量类型推断的范围。 就像校门空i守卫一样,要进门先要询问您是不是学生【if】,通过了守卫,才能进行学生具备的行为。

  3. 类型守卫发生阶段 TS条件语句中遇到下列条件关键字时,会在语句下的块级作用域内缩小变量类型的范围。类型守卫可以帮助我们在块级作用域中获得更加精确的变量类型 typeofininstanceof=====!==!=

3. TS多态

  1. 定义: 父类定义一个方法但不实现具体方法,由子类重写该方法,让子类以各自需要去实现不同的展示状态。
  2. 产生条件:
    • 必须存在于继承关系
    • 必须有方法重写
  3. 好处: 利于项目的扩展,从而局部满足了开闭原则(对修改关闭,对扩展开放)
  4. 多态的局限性: 无法直接调用子类独有的方法,必须结合instanceof类型守卫来解决
  5. 具体例子: 顾客买菜,每种蔬菜的计价方式(getTotalPrice)不同,但是顾客不需要了解各种蔬菜是怎么计价的(财大气粗的顾客),只知道蔬菜肯定是要付钱的,然后付钱就是了(遵纪守法的顾客)。
// 蔬菜类,这里最好应该用抽象类定义,下面会讲
class Vegetables {
  public price: number;
  constructor(price: number) {
    this.price = price;
  }
  getTotalPrice(): number {
    return 0;
  }
}

class Radish extends Vegetables {
  public num: number;
  constructor(price: number, num: number) {
    super(price);
    this.num = num;
  }

  // 萝卜按个数卖
  getTotalPrice(): number {
    return this.price * this.num;
  }
}

class Cauliflower extends Vegetables {
  public weight: number;
  constructor(price: number, weight: number) {
    super(price);
    this.weight = weight;
  }

  // 菜花按斤卖
  getTotalPrice(): number {
    return this.price * this.weight;
  }
}

class Client {
  pay(vegetables: Vegetables) {
    // 更具传进来来的具体蔬菜,调用具体的计价方式
    const price = vegetables.getTotalPrice();
    // 下面的支付操作
    // .....
  }
}

const radish = new Radish(12, 1);
const flower = new Cauliflower(12, 3);
const client = new Client();
client.pay(radish);
client.pay(flower);

4. 抽象类

  1. 定义: 一个在任何位置都无法被实例化的类。在基类的基础上再进行抽象,当基类没有被实例化的需求时,可以采用抽象类。抽象类在基类的基础(可以有具体方法、实例属性、构造器)之上,增加了对属性的抽象,只需在属性名前面用abstract修饰并定义具体属性的类型,在基础中起到与接口相似的功能。抽象属性可以在抽象类自身中被调用。 抽象类的功能:基类 + 接口 - 被实例化

    abstract class Animal {
     // 抽象属性
     public abstract name: string
     public sex: string
     constructor(sex: string) {
       this.sex = sex
     }
     // 具体方法,在使用访问允许的情况下,可以被子类调用/重写
     eat() {
       console.log('chi');
       // 可以调用抽象方法
       this.run();
     }
     // 抽象方法,abstract修饰,只有方法签名
     abstract run(): void
    }
    
    class Dog extends Animal {
      public name: string;
      constructor(name: string, sex: string) {
        super(sex);
        this.name = name
      }
    
      run(): void {
       console.log('四脚跑');
      }
    }
    const dog = new Dog('zhang', 'nan')
    dog.eat(); // chi
    dog.run(); // '四脚跑'
    
  2. 好处:

    • 有统一的抽象方法,提高代码的可维护性:子类去继承抽象类时,就强制我们去实现抽象类中抽象属性。
    • 避免去实现一个没有意义的类
  3. 具体例子,上述多态中的例子,蔬菜类是一个空乏的类,当我们去实例化蔬菜类似,发现这破玩意实例化了没啥用,且计价方式应该是每个子类都需要强制实现的,综上,我们应该采用抽象类。

     abstract class Vegetables {
       public abstract price: number;
       public abstract getTotalPrice(): number;
     }
    

5. 自定义类型守卫

  1. 格式:
    function 函数名(形参: 类型): 形参 is A类型(具体意义:当返回为true的时候,形参的类型即为A类型) {
      return true/false;
    }
    
  2. 实际应用
    let obj = {
      str: '123132',
      a: []
    }
    
    Object.keys(obj).forEach(key => {
     /**
      * 直接使用会报这样的错误: 
      * 元素隐式具有 "any" 类型,因为类型为 "string" 的表达式不能用于索引类型 "{ str: string; a: never[]; } "。
      * 在类型 "{ str: string; a: never[]; }" 上找不到具有类型为 "string" 的参数的索引签名。
      * 原因:在这里的key的类型为“string”,明显过于广泛。key的类型应该是每一个key名的联合类型,这里就可以使用自定义守卫
      */
      const value = obj[key];
     })
    
     // 采用自定义守卫,如下
     // 当key在obj中时,返回true,这时候自定义守卫将key的类型转化为每一个key名的联合类型
     // 这里的typeof是TS中操作符,与JS的不一样。能够反推定义的变量/常量的类型
     // keyof, 用于获取某种类型的所有key,其返回类型是联合类型。
     function isValidKey(key: string | number | symbol, object: object): key is keyof typeof object {
       return key in object;
     }
    
     // 不报错, 正常
     Object.keys(obj).forEach(key => {
       if (isValidKey(key, obj)) {
         const value = obj[key];
       }
     })
    

6. as const的运用

  1. 使用场景 当我们用const去定义数组时,只是不能再次给该数组常量赋值,且能改变数组元素. 当我们希望数组常量的元素不能改变时,可以使用as const

    const arr = [1, 2, 3] as const;
    
    // 当arr作为参数传入时,对应的方法形参类型前需要readonly修饰
    function showArr(arr: readonly any[]) {
      log(arr)
    }