TypeScript专题之泛型

云码农 (CloudCoder)
• 阅读 1963

泛型在其他很多语言中广泛地得到使用,如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中非常核心的一个技术点,作为前端程序员,必须要去掌握的一个点
  • 泛型是对类型的编程
点赞
收藏
评论区
推荐文章
Wesley13 Wesley13
3年前
java 泛型详解
对java的泛型特性的了解仅限于表面的浅浅一层,直到在学习设计模式时发现有不了解的用法,才想起详细的记录一下。本文参考java泛型详解、Java中的泛型方法、java泛型详解1\.概述泛型在java中有很重要的地位,在面向对象编程及各种设计模式中有非常广泛的应用。什么是泛型?为什么要使用泛型?泛型,即“参数化类型”。一提到参数,最熟
Wesley13 Wesley13
3年前
java泛型
一、实现机制java泛型实现方法为类型擦除,基于这种方法实现的泛型称为伪泛型。java泛型只在源代码中存在,在编译后的文件中替换为原生类型,并插入强制转换。(真正的泛型是应该存在于源码、编译后文件、运行期)二、擦除实例源码:List<StringtestListnewArrayList<String();
Stella981 Stella981
3年前
Gson通过借助TypeToken获取泛型参数的类型的方法
最近在使用Google的Gson包进行Json和Java对象之间的转化,对于包含泛型的类的序列化和反序列化Gson也提供了很好的支持,感觉有点意思,就花时间研究了一下。由于Java泛型的实现机制,使用了泛型的代码在运行期间相关的泛型参数的类型会被擦除,我们无法在运行期间获知泛型参数的具体类型(所有的泛型类型在运行时都是Object类型)。但是有的时候
Wesley13 Wesley13
3年前
Java泛型详解
引言Java泛型是jdk1.5中引入的一个新特性,泛型提供了编译时的类型检测机制,该机制允许程序员在编译时检测到非法的类型。泛型是Java中一个非常重要的知识点,在Java集合类框架中泛型被广泛应用。本文我们将从零开始来看一下Java泛型的设计,将会涉及到通配符处理,以及让人苦恼的类型擦除。泛型基础
Wesley13 Wesley13
3年前
Java泛型的使用
泛型的定义:泛型,就是允许在定义类、接口时通过一个标识表示类中某个属性的类型或者是某个方法的返回值及参数类型。这个类型参数将在使用时(例如,继承或实现这个接口,用这个类型声明变量、创建对象时)确定(即传入实际的类型参数,也称为类型实参)。泛型的引入背景:集合容器类在设计阶段或声明阶段不能确定这个容器到底实际存储的是什么类型的对象
Easter79 Easter79
3年前
Thinking in java Chapter15 泛型
1与C比较2简单泛型泛型类3泛型接口4泛型方法5匿名内部类6构建复杂模型78910“泛型”意思就是:适用于许多许多的类型<h2id"1"1与C比较</h2C
Wesley13 Wesley13
3年前
Java的泛型详解(一)
Java的泛型详解(一)编写的代码可以被不同类型的对象所重用。因为上面的一个优点,泛型也可以减少代码的编写。1|2泛型的使用简单泛型类publicclassPair{privateTfirst;privateTsecond;publicPair(){firstnull;secondnull;
Wesley13 Wesley13
3年前
C++:模板类
22.模板类22.1模板类模板是泛型编程的基础,那什么是泛型编程呢?泛型编程是一种独立于任何特定数据类型编写代码的方式。C标准模板库中的数据容器、迭代器和算法,都是泛型编程的例子,它们都使用了模板的概念。比如动态数组vector是可以存放任何类型数据的容器,我们可以定义许多不同类型的vector,比如vector<int或vect
Wesley13 Wesley13
3年前
Java泛型一览笔录
1、什么是泛型?泛型(Generics)是把类型参数化,运用于类、接口、方法中,可以通过执行泛型类型调用分配一个类型,将用分配的具体类型替换泛型类型。然后,所分配的类型将用于限制容器内使用的值,这样就无需进行类型转换,还可以在编译时提供更强的类型检查。2、泛型有什么用?泛型主要有两个好处:(1)消除显
Wesley13 Wesley13
3年前
JAVA 泛型中的通配符 T,E,K,V 傻傻分不清楚 ?
前言Java泛型(generics)是JDK5中引入的一个新特性,泛型提供了编译时类型安全检测机制,该机制允许开发者在编译时检测到非法的类型。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。泛型带来的好处在没有泛型的情况的下,通过对类型Object的引用来实现参数的“任意化”,“任意化”带
小万哥 小万哥
2年前
C++模板和泛型编程详解
C中的模板和泛型编程是非常重要的概念。模板是一种将数据类型作为参数的通用程序设计方法。它们允许开发人员编写可以处理各种数据类型的代码,而无需为每种数据类型编写不同的代码。下面介绍了一些关于C中模板和泛型编程的重要知识点模板的定义模板是一种通用程序设
云码农 (CloudCoder)
云码农 (CloudCoder)
Lv1
至少以后跟等风来随雨过也陪着我
文章
3
粉丝
0
获赞
0