一、概述
软件工程中,我们不仅要创建一致的定义良好的API,同时也要考虑可重用性。 组件不仅能够支持当前的数据类型,同时也能支持未来的数据类型,这在创建大型系统时为你提供了十分灵活的功能。泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。
先来看一组示例,定义一个函数,传入任意类型的数据并将其返回,你可能会想到使用 any
,大致如下:
function itself(arg: any): any {
return arg;
}
使用 any
类型会导致这个函数可以接收任何类型的 arg
参数,这样就丢失了一些信息,无法保证传入的类型与返回的类型是相同的。如果我们传入一个数字,我们只知道任何类型的值都有可能被返回,如果传递的是一个数值类型的数据,那么返回值的类型也应该是一个数值类型,而使用 any
,返回值为字符串类型的数据也能通过代码检测,那就无法满足我们此时需求。
因此,我们需要一种方法使返回值的类型与传入参数的类型是相同的。 这里,我们使用了 类型变量,它是一种特殊的变量,只用于表示类型而不是值。
function itself<T>(arg: T): T {
return arg;
}
我们给 itself
添加了类型变量 T
。 T
帮助我们捕获用户传入的类型(比如:number
),之后我们就可以使用这个类型。 然后我们再次使用了 T
当做返回值类型。现在我们可以知道参数类型与返回值类型是相同的了。 这允许我们跟踪函数里使用的类型的信息。
我们把这个版本的 itself
函数叫做泛型函数,因为它可以适用于多个类型。 不同于使用 any
,它不会丢失信息,传入什么类型就返回什么类型。
定义好泛型之后,有两种方法可以使用:
1)明确指定泛型类型
let name = itself<string>("myString");
2)类型推断
let name = itself("myString");
注意:我们没必要使用尖括号(
<>
)来明确地传入类型;编译器可以查看myString
的值,然后把T
设置为它的类型。 类型推论帮助我们保持代码精简和高可读性。
二、泛型变量
在函数内部使用泛型变量的时候,由于事先不知道它是哪种类型,所以不能随意的操作它的属性或方法:
function itself<T>(arg: T): T {
console.log(arg.length); // Property 'length' does not exist on type 'T'.
return arg;
}
上例中,泛型 T
不一定包含属性 length
,所以编译的时候报错了。
现在假设我们想操作 T
类型的数组,所以 .length
属性肯定是存在的。 我们可以像创建其它数组一样创建这个数组:
function itself<T>(arg: T[]): T[] {
console.log(arg.length); // => Array has a .length, so no more error
return arg;
}
你可以这么理解这段代码:函数 itself
,接受一个 泛型 T
和一个参数 arg
,其中,参数 arg
的类型 和 返回值类型为 元素类型为 T
的数组。所以,数组元素的类型具体是什么,取决于泛型变量 T
的值,如果你传入的是 number
,比如:
itself<number>([1, 2, 3]);
则相当于 itself
的类型变成了如下形式:
function itself<number>(arg: number[]): number[]
这可以让我们把泛型变量 T
当做类型的一部分使用,而不是整个类型,增加了灵活性。
我们也可以这样实现上面的例子:
function itself<T>(arg: Array<T>): Array<T> {
console.log(arg.length);
return arg;
}
三、泛型接口
在开发中,我们通常会封装 ajax
请求,统一处理后端响应,为了使得后端响应的数据在使用时能够具备TS智能提示,我们会指定返回值类型。一般来讲,后端返回数据的基本结构大致如下:
{
code: 0, // 状态码
data: null, // 响应数据
msg: '' // 响应信息
}
其中,code
和 msg
的类型是明确的,但是 data
的类型却不尽相同,这个和你的具体业务有关,比如你请求商品列表和你执行登录返回的数据肯定是不一样的,所以这里我们就不能把 data
的类型定死, 这个时候我们就可以使用泛型了,data
的类型在执行具体业务时指定。比如:
/** 泛型接口:定义后端返回结构的大致形状,其中data的类型指定为泛型T */
interface BaseResponse<T> {
code: number;
data: T
msg: string;
};
/** 登录接口所需要的参数类型 */
interface LoginParams {
account: string;
password: string;
}
/** 登录接口响应的数据类型 */
interface LoginResult {
token: string;
}
/** 调用登录接口 */
function login(data: LoginParams) {
return axios.post<BaseResponse<LoginResult>>('/api/login', data);
}
上述示例中,定义了一个泛型接口 BaseResponse
用于描绘后端返回数据的基本形状。然后封装了一个 login
函数,该函数接收一个 类型为 LoginParams
的参数 data
,在函数内部,我们通过 axios.post
和后端通信,执行登录请求。可以看到,axios.post
接收一个泛型用于指定响应类型,这里我们传入 BaseResponse
并且指定 data
的类型为 LoginResult
,所以此时,BaseResponse
的类型就会被解析成:
{
code: number;
data: {
token: string;
}
msg: string;
}
四、泛型类
与泛型接口类似,泛型也可以用于类的类型定义中:
class GenericNumber<T, U> {
zeroValue?: T;
message?: U;
add?: (x: T, y: T) => T;
}
let myGenericNumber = new GenericNumber<number, string>();
myGenericNumber.zeroValue = 0;
myGenericNumber.message = "Hello";
myGenericNumber.add = function(x, y) { return x + y; };
五、泛型参数的默认类型
在 TypeScript 2.3 以后,我们可以为泛型中的类型参数指定默认类型。当使用泛型时没有在代码中直接指定类型参数,从实际值参数中也无法推测出时,这个默认类型就会起作用
function createArray<T = string>(length: number, value: T): Array<T> {
let result: T[] = [];
for (let i = 0; i < length; i++) {
result[i] = value;
}
return result;
}
六、泛型约束
你应该会记得之前的一个例子,我们有时候想操作某类型的一组值,并且我们知道这组值具有什么样的属性。在 itself
例子中,我们想访问 arg
的 length
属性,但是编译器并不能证明每种类型都有length
属性,所以就报错了。
function itself<T>(arg: T): T {
console.log(arg.length); // Property 'length' does not exist on type 'T'.
return arg;
}
相比于操作 any
所有类型,我们想要限制函数去处理任意带有.length
属性的所有类型。 只要传入的类型有这个属性,我们就允许,就是说至少包含这一属性。 为此,我们需要列出对于 T
的约束要求。
为此,我们定义一个接口来描述约束条件。 创建一个包含 .length
属性的接口,使用这个接口和extends
关键字来实现约束:
interface Lengthwise {
length: number;
}
function itself<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
现在这个泛型函数被定义了约束,因此它不再是适用于任意类型:
itself(1); // Argument of type 'number' is not assignable to parameter of type 'Lengthwise'.
我们需要传入符合约束类型的值,必须包含必须的属性:
itself({length: 1, value: [1]});