泛型在其他很多语言中广泛地得到使用,如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
}
}
为了保证sVal
与nVal
的后续操作中类型检查的有效性,它们也会有类型(这里暂时先以显示的方式定义其类型)。
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
类。实现了如下功能点:
add
:存入一个元素;contains
:查看是否包含某一元素;remove
:删除某个元素;removeAll
:删除所有元素;foreEach
:遍历所有元素;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中非常核心的一个技术点,作为前端程序员,必须要去掌握的一个点
- 泛型是对类型的编程
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。