首先介绍一下泛型的概念
泛型程序设计(generic programming)是程序设计语言的一种风格或范式。泛型允许程序员在强类型程序设计语言中编写代码时使用一些以后才指定的类型,在实例化时作为参数指明这些类型。
泛型是指在定义函数,接口或者类的时候,不预先定义好具体的类型,而在使用的时候在指定类型的一种特性。
一个小实例
我们来模拟一个场景:某个服务提供了一些不同类型的数据,我们需要先通过一个中间件对这些数据进行一个基本的处理(比如验证,容错等),再对其进行使用。那么用 JavaScript 来写应该是这样的
JavaScript 源码
// 模拟服务,提供不同的数据。这里模拟了一个字符串和一个数值
var service = {
getStringValue: function() {
return "a string value";
},
getNumberValue: function() {
return 20;
}
};
// 处理数据的中间件。这里用 log 来模拟处理,直接返回数据当作处理后的数据
function middleware(value) {
console.log(value);
return value;
}
// JS 中对于类型并不关心,所以这里没什么问题
var sValue = middleware(service.getStringValue());
var nValue = middleware(service.getNumberValue());
改写成 TypeScript
先来看看对服务的改写,TypeScript 版的服务有返回类型:
const service = {
getStringValue(): string {
return "a string value";
},
getNumberValue(): number {
return 20;
}
};
为了保证在对 sValue
和 nValue
的后续操作中类型检查有效,它们也会有类型(如果 middleware
类型定义得当,可以推导,这里我们先显示定义其类型)
const sValue: string = middleware(service.getStringValue());
const nValue: number = middleware(service.getNumberValue());
现在的问题是 middleware
要怎么样定义才能即可能返回 string
,又可能返回 number
,而且还能被类型检查正确推导出来?
第 1 个办法,用 any
function middleware(value: any): any {
console.log(value);
return value;
}
上面这个办法可以检查通过。但它的问题在于 Any 类型会避开类型的检查,在后在对
Value
赋值的时候,也只是当作类型没有问题。简单的说,是有“假装”没问题。所以假如输入和输出不是一样的类型,typescript也不会报错。
第 2 个办法,多个 middleware
function middleware1(value: string): string { ... }
function middleware2(value: number): number { ... }
当然也可以用 TypeScript 的重载(overload)来实现
function middleware(value: string): string;
function middleware(value: number): number;
function middleware(value: any): any {
// 实现一样没有严格的类型检查
}
这种方法最主要的一个问题是……如果我有 10 种类型的数据,就需要定义 10 个函数(或重载),那 20 个,200 个呢……
正解:使用泛型(Generic)
现在我们切入正题,用泛型来解决这个问题。那么这就需要解释一下什么是泛型了:泛型就是指定一个表示类型的变量,用它来代替某个实际的类型用于编程,而后通过实际调用时传入或推导的类型来对其进行替换,以达到一段使用泛型程序可以实际适应不同类型的目的。
虽然这个解释已经很接地气了,但是理解起来还是不如一个实例来得容易。我们来看看 middleware
的泛型实现是怎么样的
function middleware(value: T): T {
console.log(value);
return value;
}
middleware
后面紧接的
表示声明一个表示类型的变量,Value: T
表示声明参数是 T
类型的,后面的 : T
表示返回值也是 T
类型的。那么在调用 middlewre(getStringValue())
的时候,由于参数推导出来是 string
类型,所以这个时候 T
代表了 string
,因此此时 middleware
的返回类型也就是 string
;而对于 middleware(getNumberValue())
调用来说,这里的 T
表示了 number
。
我们直接从 VSCode 的提示可以看出来,对于 middleware
调用,TypeScript 可以推导出参数类型和返回值类型:
我们也可以在调用的时候,小括号前显示指定 T
代替的类型,比如 mdiddleware
,不过如果指定的类型与推导的类型有冲突,就会提示错误:
泛型语法
泛型即可以声明函数, 也可以声明类. 也可以声明接口
class Person{} // 一个尖括号跟在类名后面
function Person(arg: T): T {return arg;} // 一个尖括号跟在函数名后面
interface Person {} // 一个尖括号跟在接口名后面
多个类型参数
我们在定义范型的时候,也可以一次定义多个类型参数,像下面这样。
function swap(tuple: [T, U]):[U, T] {
return [tuple[1], tuple[0]];
}
泛型接口
我们先定义一个范型接口Identities,然后定义一个函数identities()来使用这个范型接口
interface Identities {
id1: T;
id2: U;
}
我在这里使用T和U作为我们的类型变量来演示任何字母(或有效的字母数字名称的组合)都是有效的类型—除了常规用途之外,您对它们的调用没有任何意义。
我们现在可以将这个接口应用为identity()的返回类型,修改我们的返回类型以符合它。我们还可以console.log这些参数和它们的类型,以便进一步说明:
function identities (arg1: T, arg2: U): Identities {
console.log(arg1 + ": " + typeof (arg1));
console.log(arg2 + ": " + typeof (arg2));
let identities: Identities = {
id1: arg1,
id2: arg2
};
return identities;
}
我们现在对identity()所做的是将类型T和U传递到函数和identity接口中,从而允许我们定义与参数类型相关的返回类型。
范型变量
使用泛型创建像identity这样的泛型函数时,编译器要求你在函数体必须正确的使用这个通用的类型。 换句话说,你必须把这些参数当做是任意或所有类型。
我们先看下之前例子
function genericDemo(data: T):T {
return data;
}
如果我们想同时打印出data的长度。 我们很可能会这样做
function genericDemo(data: T):T {
console.log(data.length); // Error: T doesn't have .length
return data;
}
如果这么做,编译器会报错说我们使用了data的.length属性,但是没有地方指明data具有这个属性。 记住,这些类型变量代表的是任意类型,所以使用这个函数的人可能传入的是个数字,而数字是没有.length属性的。
现在假设我们想操作T类型的数组而不直接是T。由于我们操作的是数组,所以.length属性是应该存在的。 我们可以像创建其它数组一样创建这个数组:
function genericDemo(data: Array):Array {
console.log(data.length);
return data;
}
范型类
前面已经解释了“泛型”这个概念。示例中泛型的用法我们称之为“泛型函数”。不过泛型更广泛的用法是用于“泛型类”——即在声明类的时候声明泛型,那么在类的整个作用域范围内都可以使用声明的泛型类型。
相信大家都已经对数组有所了解,比如 string[] 表示字符串数组类型。其实在早期的 TypeScript 版本中没有这种数组类型表示,而是采用实例化的泛型 Array
除此之外,TypeScript 中还有一个很常用的泛型类,Promise
所以,泛型类其实多数时候是应用于容器类。假设我们需要实现一个 FilteredList,我们可以向其中 add()(添加) 任意数据,但是它在添加的时候会自动过滤掉不符合条件的一些,最终通过 get all() 输出所有符合条件的数据(数组)。而过滤条件在构造对象的时候,以函数或 Lambda 表达式提供。
// 声明泛型类,类型变量为 T
class FilteredList {
// 声明过滤器是以 T 为参数类型,返回 boolean 的函数表达式
filter: (v: T) => boolean;
// 声明数据是 T 数组类型
data: T[];
constructor(filter: (v: T) => boolean) {
this.filter = filter;
}
add(value: T) {
if (this.filter(value)) {
this.data.push(value);
}
}
get all(): T[] {
return this.data;
}
}
// 处理 string 类型的 FilteredList
const validStrings = new FilteredList(s => !s);
// 处理 number 类型的 FilteredList
const positiveNumber = new FilteredList(n => n > 0);
甚至还可以把 (v: T) => boolean 声明为一个类型,以便复用
type Predicate = (v: T) => boolean;
class FilteredList {
filter: Predicate;
data: T[];
constructor(filter: Predicate) { ... }
add(value: T) { ... }
get all(): T[] { ... }
}
当然类型变量也不一定非得叫 T,也可以叫 TValue 或别的什么,但是一般建议以大写的 T 作为前缀,采用 Pascal 命名规则,方便识别。还有一些常见的指代,比如 TKey 表示键类型,TValue 表示值类型等(常用于映射表这类容器定义)。
我们还可以在类属性和方法的意义上使类泛型。泛型类确保在整个类中一致地使用指定的数据类型。例如下面这种在React Typescript项目中的写法。
interface Props {
className?: string;
...
}
interface State {
submitted?: bool;
...
}
class MyComponent extends React.Component {
...
}
我们在这里使用与React组件一起使用的泛型,以确保组件的props和state是类型安全的。
泛型约束
我们先看一个常见的需求,我们要设计一个函数,这个函数接受两个参数,一个参数为对象,另一个参数为对象上的属性,我们通过这两个参数返回这个属性的值,比如:
function getValue(obj: object, key: string){
return obj[key] // error
}
我们会得到一段报错,这是新手 TypeScript 开发者常常犯的错误,编译器告诉我们,参数 obj 实际上是 {},因此后面的 key 是无法在上面取到任何值的。
因为我们给参数 obj 定义的类型就是 object,在默认情况下它只能是 {},但是我们接受的对象是各种各样的,我们需要一个泛型来表示传入的对象类型,比如T extends object:
function getValue(obj: T, key: string) {
return obj[key] // error
}
这依然解决不了问题,因为我们第二个参数 key 是不是存在于 obj 上是无法确定的,因此我们需要对这个 key 也进行约束,我们把它约束为只存在于 obj 属性的类型,这个时候需要借助到后面我们会进行学习的索引类型进行实现 ,我们用索引类型 keyof T 把传入的对象的属性类型取出生成一个联合类型,这里的泛型 U 被约束在这个联合类型中,这样一来函数就被完整定义了:
function getValue(obj: T, key: U) {
return obj[key] // ok
}
泛型在Http接口中的应用
在实际项目中, 每个项目都需要接口请求, 我们会封装一个通用的接口请求, 在这个函数里面, 处理一些常见的错误等等. 为了让每个接口调用都有 typescript 约束, 提醒. 这里使用泛型是非常合适了.
假如在项目开发前期,前后端规定的接口数据格式为:
{
code: 200,
message: "",
data: {}
}
所有的接口都遵从这样的格式。
- code 代表接口的成功与失败
- message 代表接口失败之后的服务端消息输出
- data 代表接口成功之后真正的逻辑
在 ajax.d.ts 文件使用泛型定义了一个 response 的类型,如:
// 使用枚举定义常量
export enum StateCode {
error = 400,
ok = 200,
timeout = 408,
serviceError = 500
}
// 定义了一个 response 的类型
export interface IResponse {
code: StateCode;
message: string;
data: T;
}
接着,在使ajax.d.ts中定义好返回值data的数据类型:
export interface IFavorites {
id: string;
img: string;
name: string;
url: string;
}
然后在请求的时候就能进行使用
this.axiosRequest({ key: 'idc' }).then((response: IResponse) => {
const { code, message, data} = response;
if (code === StateCode.ok) {
// 处理
}
})
站在巨人肩上
前端深入理解Typescript泛型概念
从 JavaScript 到 TypeScript - 泛型
大话 Typescript泛型