泛型类、泛型接口

192 阅读10分钟

TS 泛型重要性

  1. Vue3源码充斥着大量的 TS 泛型。懂 TS 泛型是读懂 Vue3 源码不可逾越的环节。
  2. 前端各种技术的声明文件【 d.ts 文件】 TS 泛型更是随处可见【例如:小到一个 Array,ES6 的 Set,Map,稍微复杂点的例如:Vue3应用的声明文件,Vuex 底层的声明文件,React 组件声明文件,axios 声明文件,这样的例子举不胜举。】
  3. 现在采用 TS 整合前端框架的大中项目越来越多。
  4. 后端 Nodejs 和 TS 整合的频次也越来越高,优秀的 Nestjs 框架就完全采用 TS 开发。

TS 泛型类好处,重构 ArrayList

  • 好处1: 编译期对类上调用方法或属性时的泛型类型进行安全检查(类型安全检查),不符合泛型实际参数类型(泛型实参类型) 就编译通不过,防止不符合条件的数据增加进来。
  • 好处2: 一种泛型类型被具体化成某种数据类型后,该数据类型的变量获取属性和方法时会有自动提示,这无疑提高代码开发效率和减少出错率。
//   1.对现有的数组进行封装,让数组增删改变得更加好用
//   2.提供get方法 remove方法 显示方法【add方法】
//  其中需求中的remove方法有两个,我们用方法重载来实现

class ArrayList<T = any> {
  // 第一步:定义一个引用属性【数组】
  public element: Array<T>
  constructor() {

    this.element = [];
  }
  public index: number = 0;
   往数组中添加元素
  public add(ele: T) {
    console.log("this.kk * 3:"this.kk * 3);
    this.checkIndex();
    this.element[this.index++] = ele;
  }
  public checkIndex() {
    if (this.index < 0) {
      throw new Error("数组下标不能为零");
    }
  }
  //  第二步:根据索引来查询数组中指定元素
  get(index: number): T {

    return this.element[index];

  }

   // 第三步: 显示方法
  show() {
    this.element.forEach((ele) => {
      console.log(ele);
    })
  }

  remove(value: number): number
  remove(value: object): object
  remove(value: number | object): number | object {
  remove(value: any): any {
    this.element = this.element.filter((ele, index) => {
      // 如果是根据数字【元素索引】去删除元素,remove方法返回的是一个数字
      if (typeof value === "number") {
        return value !== index
      } else {
        //  如果是根据对象去删除元素,remove方法返回的是一个对象
        return value !== ele
      }
    })
    return value;
  }

}
type stuType = { stuname: string, age: number, address: string }
let stuOne: stuType = { stuname: "wnagwu"age: 23address: "beijing" }
let stuTwo: stuType = { stuname: "lisi"age: 39address: "shanghai" }
let stuThree: stuType = { stuname: "liuqi"age: 31address: "nanjing" }

let arrayList = new ArrayList([stuOne, stuTwo, stuThree]);
let arrayList = new ArrayList<number>();
arrayList.add(3)

console.log(arrayList.get(0));

let arrayList2 = new ArrayList();
arrayList2.add(3)

let arrayList3 = new ArrayList<typeof stuOne>();
let arrayList3 = new ArrayList<stuType>();
arrayList3.add(stuOne)
arrayList3.add(stuTwo)
arrayList3.add(stuThree)

let stuobj = arrayList3.get(1)
console.log(stuobj.stuname);

// 泛型如果在使用时没有具体化的类型,并且定义泛型时使用了T=any,那么就会选择any数据类型
let arrayList5 = new ArrayList();
arrayList5.add(3)
arrayList5.add("abc");
arrayList5.add(stuOne);
arrayList5.get(0).
let stuObj2 = arrayList5.get(2);
console.log("stuObj2.stuname:", stuObj2.stuname)
export { }

TS 泛型定义、泛型类的格式

  1. 泛型一种参数化数据类型,具有以下特点的数据类型叫泛型 :

    • 特点一:定义时不明确使用时必须明确成某种具体数据类型的数据类型。【泛型的宽泛】
    • 特点二:编译期间进行数据类型安全检查的数据类型。【泛型的严谨】
  2. 特别注意:

    1. 类型安全检查发生在编译期间
    2. 泛型是参数化的数据类型, 使用时明确化后的数据类型就是参数的值
  3. 泛型类的格式

	class 类名<泛型形参类型> 泛型形参类型一般有两种表示: 1. A-Z 任何一个字母 
        2. 语义化的单词来表示,绝大多数情况,泛型都是采用第一种形式表示,如下:	
    class ArrayList<T>{  
         array: Array<T>
         add(data:T){
             ....
         }
		 ....
    }

object为什么不能替代类上的泛型?

原因一:编译期间 object 无法进行类型安全检查,而泛型在编译期间可以进行类型安全检查 object 接受也只能接受所有的 object 类型的变量,比如有 Customer、Student、Dog 类的实例都是对象类型,或者自己定义的对象,都可以传递给 object 类型的方法参数或属性, 但如果我们只希望添加Customer类的对象,当添加其他类的对象必须出现编译错误,但是 object 无法做到,就只能用泛型了。

原因二: object 类型数据无法接受非 object 类型的变量,只能接受 object 类型的变量,泛型能轻松做到

正因为 object 接受也只能接受所有的 object 类型的变量,那么如果有一个集合类[数组封装类]有一个 add 方法,允许每次添加指定类型的变量到 add 方法的参数,比如:我们第一轮的希望添加 10 次字符串类型的变量,第二轮的希望添加 10 次整数类型变量,第三轮的希望添加 10 次顾客类型的变量,泛型轻松做到。object 类型数据无法接受任意非 object 类型的变量,object 只能接受所有的 object 类型的变量。

原因三: object 类型数据获取属性和方法时无自动提示,泛型有自动提示

一种泛型类型被具体化成某种数据类型后,该数据类型的变量获取属性和方法时会有自动提示,提高代码开发效率和减少出错率,但在 object 类型的变量无法获取数据类型的属性和方法,降低了体验感和开发效率。

any 为什么不能替代类上的泛型?

  • 原因一:编译期间 any 无法进行类型安全检查,而泛型在编译期间可以进行类型安全检查。我们学过: any 是所有类型的父类,也是所有类型的子类如果我们现在是一个宠物店类,希望只能添加 Dog 类,当调用 add 方法添加 Customer、Student 类必定出现编译错误,从而保证了类型安全检查,但是 any 类型无法保证类型安全检查,可以为任意类型,包括 string,number,boolean,null,undefined,never,void,unknown 基础数据类型和数组,类,接口类型, type 类型的变量全部能接受,不会进行无法进行类型安全检查。
  • 原因二:any 类型可以获取任意数据类型的任何属性和任意方法而不会出现编译错误导致潜在错误风险,而泛型却有效的避免了此类问题发生。any 类型可以获取任何属性和任意方法而不会出现编译错误,因为any可以代表任意数据类型来获取任意属性和任意方法,但是泛型类型被具体化成某种数据类型后,该数据类型的变量调用该数据类型之外的属性和方法时,出现编译错误,这也减少了代码隐藏潜在错误的风险。
  • 原因三: any 类型数据获取属性和方法时无自动提示,泛型有自动提示
  • 彩蛋:any 类型可以代表任意数据类型来获取任何属性和任意方法而不会出现编译错误,因为any可以代表任意数据类型来获取任意属性和任意方法:【 any 的这个特性是一把双刃剑,当我们需要这么使用,它给我们带来方便,但是大多数情况下我们是不需要这么做的】。

T extends object

  1. 理解T extends object:extends 表示具体化的泛型类型只能是 object 类型,某个变量如果能断言成 object 类型【变量 as object】,那么这个变量的类型就符合 T extends object 。就是说该变量的类型可以是T的具体化类型。任何一个类或者构造函数的底层都是从 new Object ( )而来,这个 new Object ( )对象的类型就是 object 类型。这就是说任何类的对象或者构造函数的对象都符合T extends object。
  2. 分页类使用 T extends object 的原因:分页类中只添加对象数据,所以泛型要被具体化成一个对象类型才符合要求,例如多个顾客对象,多个美食对象,而拒绝添加 string,number,其他数据类型。

Pager 类:

class Pager<M> {

  firstRecordNoCurPage!: number;//每一页的第一条记录号是多少
  pageSize: number = 3;//每一页总共有几条记录
  pageCount: number = 0;// 当前是第几页--从前端页面传递过来的数据
  dataList!: ArrayList<M>;// 封装数据表取出来的全部数据的集合类【等外部传递数据给dataList】

  constructor(pageCount: number) {
    this.pageCount = pageCount;
  }

  // 显示当前页的数据
  public showCurrentPageData() {
    // 当前页的第一条记录号是多少
    this.firstRecordNoCurPage = this.pageSize * (this.pageCount - 1)
    // 当前页的最后一条记录号
    let lastRecordNoCurPage = this.firstRecordNoCurPage + this.pageSize - 1
    //  当前页的所有记录

    //let resultDataListCurpage = lastRecordNoCurPage >= this.dataList.size() - 1 ?
    // 如果lastRecordNoCurPage计算是按照每一页3条记录计算出来的最后一页的最后一条记录,
    // 如果最后一页小于3条记录, 就直接slice到this.dataList.size()就可以了
    return lastRecordNoCurPage >= this.dataList.size() - 1 ?
      this.dataList.element
        .slice(this.firstRecordNoCurPage, this.dataList.size())
      : this.dataList.element
        .slice(this.firstRecordNoCurPage, lastRecordNoCurPage + 1)
    //12  14+1  12 13 14
    //return resultDataListCurpage;
  }

ArrayList 类:

class ArrayList<T = any> {
  //第一步:定义一个引用属性【数组】
  public element: Array<T>
  constructor() {
    this.element = []
  }


  public index: number = 0;

  public add(ele: T) {
    //console.log("this.kk * 3:", this.kk * 3);
    this.checkIndex();
    this.element[this.index++] = ele;
  }
  public checkIndex() {
    if (this.index < 0) {
      throw new Error("数组下标不能为零");
    }
  }

  public size() {
    return this.element ? this.element.length : 0;
  }
  get(index: number): T {

    return this.element[index];

  }

  show() {
    this.element.forEach((ele) => {
      console.log(ele);
    })
  }

  remove(value: number): number
  remove(value: object): object
  remove(value: any): any {
    this.element = this.element.filter((ele, index) => {
      //如果是根据数字【元素索引】去删除元素,remove方法返回的是一个数字
      if (typeof value === "number") {
        return value !== index
      } else {
        // 如果是根据对象去删除元素,remove方法返回的是一个对象
        return value !== ele
      }
    })
    return value;
  }
}

T extends object + extends keyof

  1. 理解 keyof:keyof 表示获取一个或者一个对象类型 或者一个接口类型的所有属性名[ key ]组成的联合类型。[如果类或者对象类型或者接口上只有一个属性,那么就是一个单一的属性名的类型]
  2. T extends object + extends keyof 组合使用带来的好处:类型安全检查,自动提示。
class ObjectImpl<T extends object, K extends keyof T>{
  object!: T;
  key!: K
  constructor(object_: T, key_: K) {
    this.object = object_;
    this.key = key_;
  }
  getValue() {
    return this.object[this.key]
  }
  setValue(newVal: T[K]) {
    this.object[this.key] = newVal;
  }
}

TS 泛型接口+ TS 泛型类

  1. 先了解 ArrayList 和 LinkedList

    • ArrayList和LinkedList,ES6 的 Set 都属于功能相同但是实现方式不同的集合类
    • ArrayList 应用场景:ArrayList ,Set 底层都是基于数组二次封装的类,所以查询效率很高,但插入,更新,删除数据的效率低 。
    • LinkedList 链表的应用场景:基于链表结构,插入,删除数据的效率高【尤其是插入数据效率更高】,但查询效率低,另外 LinkedList 提供了 addFirst【添加元素到首位】,addLast 等更多灵活添加数据的方法**,如果addFirst 用 ArrayList 来实现,效率就很低,因为需要把数组每一个元素进行往后移动位置,但 LinkedList实现只需要改变首个节点的prev和新插入节点的 next 即可,效率比 ArrayList 高出许多。
    • 如果某项目需要频繁插入,更新,添加操作,那么就需要使用 LinkedList,比如:新闻项目,股票系统;而对于查询量大,数据变化小的项目就要 ArrayList,比如人口普查系统。
  2. 为什么要用接口丶泛型

    • 好处1:降低代码管理成本,提供统一属性和方法命名。 为实现了本接口的所有泛型类提供了统一的方法和属性,避免了不同的泛型类对于相同功能的方法和属性各自命名导致加大代码管理和测试的成本的问题。
    • 好处2:可以从整体上快速通读类的共同方法和属性。 可以把接口当成一个书的目录,快速查看类的方法和属性,这对于首次了解项目的开发人员来说是大为有好处的。
    • 好处3:新增相同功能类时,可以快速搭建类的方法。
    • 好处4:和多态结合增加了项目的扩展性和简洁度,对开发大中项目有好处

TS 泛型接口+ TS 泛型类+泛型约束+多态的应用

export default interface List<T> {

  add(ele: T): void;
  get(index: number): T;
  size(): number;
  remove(value: T): T;

}
class Customer {
  // 实现功能,可以租赁多个交通工具,计算租金
  // public rentVechile(myVechileArray: List<Vechile>) {
  //   let lasttotal = 0;
  //   for (let i = 0; i < myVechileArray.size(); i++) {
  //     lasttotal += myVechileArray.get(i).calculateRent();
  //   }
  //   return lasttotal;
  //   //myVechileArray.calculateRent();
  // }

  // 可以租赁任意物品,计算租金
  public rent<T extends object>(list: List<T>) {
    let lasttotal = 0;
    for (let i = 0; i < list.size(); i++) {
      let something = list.get(i);
      lasttotal += (something as any).calculateRent();
    }
    return lasttotal;
  }

}

// 多态体现在: 1.父类对象变量可以接受任何它的子类对象  
//  2. 接口类型对象变量可以接受任何它的实现类的对象

let vechileList: List<Vechile> = new LinkedList<Vechile>();
let bus: Vechile = new Bus("大巴", "京3A556", 3, 50000, 67);
vechileList.add(bus);
vechileList.add(new Truck("大卡车", "京7A556", 2, 60000, 1500));
let cust = new Customer();
let lasttotal = cust.rent<Vechile>(vechileList); // 可以指定泛型
//let lasttotal = cust.rent(vechileList); // 也可以自动推导
console.log("lasttotal:", lasttotal)

// 不能存储string类型,所以要有泛型约束
//let stringList: List<string> = new ArrayList<string>();
//let dd = cust.rent<string>(stringList);