1

泛型在其他很多语言中广泛地得到使用,如Java、C++、.Net、C#等。它是程序设计语言的一种风格或范式。允许我们在编写代码的时候使用一些以后才指定的类型,在实例化时作为参数指明这些类型。而不同语言对于泛型的实现是不同的。

泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。

泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。

为什么需要泛型

服务中间件处理数据

有这么一个场景:某个服务提供了一些不同类型的数据,我们需要先通过一个中间件对这些数据进行一个基本的处理(比如验证,容错等),再对其进行使用。

JavaScript实现:

// 模拟服务,提供不同的数据
const service = {
  getStringValue: function() {
    return 'a string value'
  },
  getNumberValue: function() {
    return 888
  }
}

// 处理数据的中间件。这里用Log来模拟处理,直接返回数据当作处理后的数据
function middleware(val) {
  console.log(val)
  return val
}

let sVal = middleware(service.getStringValue())
let nVal = middleware(service.getNumberValue())

那将上面的代码改写成TypeScript实现怎么去写?

首先看下service对象的代码,有两个方法,均有返回值。那么改写成TypeScript后应该是这样的:

const service = {
  getStringValue: function(): string {
    return 'a string value'
  },
  getNumberValue: function(): number {
    return 888
  }
}

为了保证sValnVal的后续操作中类型检查的有效性,它们也会有类型(这里暂时先以显示的方式定义其类型)。

const sVal: string = middleware(service.getStringValue())
const nVal: number = middleware(service.getNumberValue())

那么接下来的问题就是middleware这个函数了。它要怎样定义才既可能返回string,又返回numer类型的数据,而且还能被类型检查正确推导出来?

哎,这里有如下几种方式:

  • 用any方法,缺点也很明显,就是middleware方法内部失去了类型检查。

    function middleware(value: any) {
      console.log(value)
      return value
    }
  • 多个middleware方法,缺点是假如现在有10种类型的数据,就需要定义10个函数,那200个、500个呢?没任何扩展性。

    function middleware1(value: string): string { ... }
    function middleware2(value: number): number { ... }

类似的方法重载也是一样:

function middleware(value: string): string;
function middleware(value: number): number;
function middleware(value: any): any {
    // 实现一样没有严格的类型检查
}
  • 使用泛型

    function middleware<T>(value: T): T {
      console.log(value)
      return value
    }
  • 可以看出方法middleware后紧跟着<T>表示声明一个表示类型的变量;
  • value: T表示声明参数类型是T类型;
  • : T表示返回值也是T类型

那么在调用middleware(service.getStringValue())的时候,由于参数推导出来是string类型的,所以这个时候类型T代表了string,因此此时middleware方法的返回值类型也是string类型。同理,当调用middleware(service.getNumberValue())也是如此。
上述代码参照了另一位博主的文章讲解

泛型类

前面已经对泛型有个基本的认识了。上面例子中泛型的用法我们称为"泛型函数"。不过泛型更为广的用法是用于"泛型类"——即在声明类的时候声明泛型,那么在类的整个个作用域范围内都可以使用声明的泛型类型。

在ES6中,诸如Promise、Map、Set等,其实现跟泛型关系挺大的。如Map的定义:

interface Map<K, V> {
    clear(): void;
    delete(key: K): boolean;
    forEach(callbackfn: (value: V, key: K, map: Map<K, V>) => void, thisArg?: any): void;
    get(key: K): V | undefined;
    has(key: K): boolean;
    set(key: K, value: V): this;
    readonly size: number;
}

那接下来,我们就来使用泛型来实现一个List类。实现了如下功能点:

  1. add:存入一个元素;
  2. contains:查看是否包含某一元素;
  3. remove:删除某个元素;
  4. removeAll:删除所有元素;
  5. foreEach:遍历所有元素;
  6. map:遍历所有元素,对某些元素进行处理,返回新的集合
class HcList<T> {
  private elems: T[] = []

  constructor(elems: T[] = []) {
    this.elems = elems
  }

  add(ele: T): void {
    this.elems.push(ele)
  }

  contains(ele: T): boolean {
    return this.elems.includes(ele)
  }

  remove(ele: T): void{
    this.elems = this.elems.filter(existing => existing !== ele)
  }

  removeAll(): void {
    this.elems = []
    this.elems.length = 0
  }

  forEach(func: (ele: T, index: number) => void): void {
    return this.elems.forEach(func)
  }

  map<U>(func: (ele: T, index: number) => U): HcList<U> {
    return new HcList<U>(this.elems.map(func))
  }
}

// test string
const stringList = new HcList<string>()
stringList.add('DarkCode')

// test number
const numberList = new HcList<number>()
numberList.add(88)

// test interface
interface IUser {
  name: string,
  age: number,
  sex?: number
}
const mUser: IUser = {
  name: 'huangche',
  age: 22
}
const userList = new HcList<IUser>()
userList.add(mUser)

接下来,为大家对泛型的深层理解,对这段代码进行一些重点的解释。

  • class HcList<T>: 定义一个HcList的类,并接收一个名为T的通用类型参数,这个类型T可供所有该类的成员访问
  • private elems: T[] = []:在类HcList中,使用了类型T来定义了私有属性elems,其类型是一个数组,是该类后续方法得以实现的基础
  • map<U>(func: (ele: T, index: number) => U): HcList<U>map方法需要一个自己的泛型类型参数,我们需要在map方法的签名中定义一些T类型,并通过回调函数映射到U类型,因此另一个类型参数U。因为这些类型仅在其功能的"氛围内",并且它们之间不存在共享,因此不冲突。

泛型接口

接口是对类的一种抽象,在接口中定义的方法和变量只有声明,不能在接口中实现。在TypeScript中,充分合理地利用接口能够达到代码高度复用的效果,而对接口进行泛型类型约定,也是非常常用的。往往在很多三方库的源码中看见。列如上面提到的map

interface Map<K, V> {
    clear(): void;
    delete(key: K): boolean;
    forEach(callbackfn: (value: V, key: K, map: Map<K, V>) => void, thisArg?: any): void;
    get(key: K): V | undefined;
    has(key: K): boolean;
    set(key: K, value: V): this;
    readonly size: number;
}

这里会基于泛型类说讲到的代码HcList来进行改造。

先定义一个IList接口,代码如下:

interface IList<T> {
  add(t: T): void,
  contains(t: T): boolean,
  remove(t: T): void,
  removeAll(): void,
  forEach(func: (t: T, index: number) => void): void,
  map<U>(func: (t: T, index: number) => U) : IList<U>
}

接下来写一个类HcList来实现这个接口,代码如下:

class HcListCls<T> implements IList<T>{
  private elems: T[] = []

  constructor(elems: T[] = []) {
    this.elems = elems
  }

  add(ele: T): void {
    this.elems.push(ele)
  }

  contains(ele: T): boolean {
    return this.elems.includes(ele)
  }

  remove(ele: T): void{
    this.elems = this.elems.filter(existing => existing !== ele)
  }

  removeAll(): void {
    this.elems = []
    this.elems.length = 0
  }

  forEach(func: (ele: T, index: number) => void): void {
    return this.elems.forEach(func)
  }

  map<U>(func: (ele: T, index: number) => U): HcList<U> {
    return new HcList<U>(this.elems.map(func))
  }
}

// test string
const strList = new HcListCls<string>()
strList.add('DarkCode')

// test number
const numList = new HcListCls<number>()
numList.add(88)

// test interface
interface IUser {
  name: string,
  age: number,
  sex?: number
}
const iUser: IUser = {
  name: 'huangche',
  age: 22
}
const uList = new HcListCls<IUser>()
userList.add(iUser)

泛型约束

在函数内部使用泛型变量的时候,由于事先不知道它是哪种类型,所以不能随意的操作它的属性或方法:

function loggingIdentity<T>(arg: T): T {
    console.log(arg.length);
    return arg;
}

// index.ts(2,19): error TS2339: Property 'length' does not exist on type 'T'.

上例中,泛型 T 不一定包含属性 length,所以编译的时候报错了。

这时,我们可以对泛型进行约束,只允许这个函数传入那些包含 length 属性的变量。这就是泛型约束:

interface Lengthwise {
    length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
    console.log(arg.length);
    return arg;
}

上例中,我们使用了 extends 约束了泛型 T 必须符合接口 Lengthwise 的形状,也就是必须包含 length 属性。

此时如果调用 loggingIdentity 的时候,传入的 arg 不包含 length,那么在编译阶段就会报错了:

interface Lengthwise {
    length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
    console.log(arg.length);
    return arg;
}

loggingIdentity(7);

// index.ts(10,17): error TS2345: Argument of type '7' is not assignable to parameter of type 'Lengthwise'.

多个类型参数之间也可以互相约束:

function copyFields<T extends U, U>(target: T, source: U): T {
    for (let id in source) {
        target[id] = (<T>source)[id];
    }
    return target;
}

let x = { a: 1, b: 2, c: 3, d: 4 };

copyFields(x, { b: 10, d: 20 });

上例中,我们使用了两个类型参数,其中要求 T 继承 U,这样就保证了 U 上不会出现 T 中不存在的字段。

泛型约束能够在我们使用到泛型的时候,对于类型的限制起到很重要的作用。

总结

  • 泛型是TypeScript中非常核心的一个技术点,作为前端程序员,必须要去掌握的一个点
  • 泛型是对类型的编程

前端扫地僧
2.5k 声望1.2k 粉丝

« 上一篇
Vue3初体验
下一篇 »
Vue3问题总结