一.存在意义
考虑这样一个场景,identity
函数接受一个参数,并原样返回:
function identity(arg) {
return arg;
}
从类型上看,无论参数是什么类型,返回值的类型都与参数一致,借助重载机制,可以这样描述:
function identity(arg: number): number;
function identity(arg: string): string;
function identity(arg: boolean): boolean;
// ...等无数个 a => a 的类型描述
重载似乎并不能满足这个场景,因为我们没有办法穷举arg
的所有可能类型。既然参数是任意类型,不妨用any
试试:
function identity(arg: any): any;
覆盖到了所有类型,却丢失了参数与返回值的类型对应关系(上面相当于A => B
的类型映射,而我们想要描述的是A => A
)
泛型与any
那么,应该如何表达两个any
之间的对应关系呢?
用泛型。这样描述:
function identity<T>(arg: T): T {
return arg;
}
类型变量T
与any
类似,相当于具名any,这样就能明确表达T => T
(即A => A
)的类型映射了
二.类型变量
Type variable, a special kind of variable that works on types rather than values.
普通变量代表一个值,而类型变量代表一个类型
从作用上看,变量能够搬运值,而类型变量搬运的是类型信息:
This allows us to traffic that type information in one side of the function and out the other.
三.泛型函数
类型变量也叫类型参数,与函数参数类似,区别在于函数参数接受一个具体值,而类型参数接受一个具体类型,例如:
function identity<T>(arg: T): T {
return arg;
}
// 传参给类型参数
// identity<number>
// 传参给函数参数(自动推断类型参数)
identity(1);
// 传参给函数参数(显式传入类型参数)
identity<number>(1);
带有类型参数的函数称为泛型函数,其中类型参数代表任意类型(any and all types),所以只有所有类型共有的特征才能访问:
function loggingIdentity<T>(arg: T): T {
// 报错 Property 'length' does not exist on type 'T'.
console.log(arg.length);
return arg;
}
实际上,因为有void
这个空集在,所以并不存在所有类型通用的属性或方法。也不能对类型变量做任何假设(比如假定它有length
属性),因为它代表一个任意类型,没有任何约束
除此之外,类型变量T
就像一个具体类型一样,可以用于任何具体类型出没的地方:
function loggingIdentity<T>(arg: T[]): T[] {
console.log(arg.length); // Array has a .length, so no more error
return arg;
}
// 或者
function loggingIdentity<T>(arg: Array<T>): Array<T> {
console.log(arg.length); // Array has a .length, so no more error
return arg;
}
类型描述
泛型函数的类型描述与普通函数类似:
// 普通函数
let myIdentity: (arg: string) => string =
function(arg: string): string {
return arg;
};
// 泛型函数
let myIdentity: <T>(arg: T) => T =
function<T>(arg: T): T {
return arg;
};
仍然是箭头函数语法,只是在(参数列表)
前增加了<类型参数列表>
。同样的,类型描述中类型参数名也可以与实际的不一致:
let myIdentity: <U>(arg: U) => U =
function<T>(arg: T): T {
return arg;
};
P.S.特殊的,函数类型描述还可以写成对象字面量的形式:
// 泛型函数
let myIdentity: { <T>(arg: T): T };
// 普通函数
let myIdentity: { (arg: string): string };
像是接口形式类型描述的退化版本,没有复用优势,也不如箭头函数简洁,因此,并不常见
四.泛型接口
带类型参数的接口叫泛型接口,例如可以用接口来描述一个泛型函数:
interface GenericIdentityFn {
<T>(arg: T): T;
}
还有一种非常相像的形式:
interface GenericIdentityFn<T> {
(arg: T): T;
}
这两种都叫泛型接口,区别在于后者的类型参数T
作用于整个接口,例如:
interface GenericIdentity<T> {
id(arg: T): T;
idArray(...args: T[]): T[];
}
let id: GenericIdentity<string> = {
id: (s: string) => s,
// 报错 Types of parameters 's' and 'args' are incompatible.
idArray: (...s: number[]) => s,
};
接口级的类型参数有这种约束作用,成员级的则没有(仅作用于该泛型成员)
五.泛型类
同样,带类型参数的类叫泛型类,例如:
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
}
像接口一样,泛型类能够约束该类所有成员关注的目标类型一致:
Putting the type parameter on the class itself lets us make sure all of the properties of the class are working with the same type.
注意,类型参数仅适用于类中的实例成员,静态成员无法使用类型参数,例如:
class GenericNumber<T> {
// 报错 Static members cannot reference class type parameters.
static zeroValue: T;
}
因为静态成员在类实例间共享,无法唯一确定类型参数的具体类型:
let n1: GenericNumber<string>;
// 期望 n1.constructor.zeroValue 是 string
let n2: GenericNumber<number>;
// 期望 n1.constructor.zeroValue 是 number,出现矛盾
P.S.这一点与Java一致,具体见Static method in a generic class?
六.泛型约束
类型参数太“泛”(any and all)了,在一些场景下,可能想要加以约束,例如:
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length); // Now we know it has a .length property, so no more error
return arg;
}
通过接口来描述对类型参数的约束(T extends constraintInterface
),比如上面要求类型参数T
必须具有一个number
类型的length
属性`
另一个典型的场景是工厂方法,例如:
// 要求构造函数c必须返回同一类(或子类)的实例
function create<T>(c: {new(): T; }): T {
return new c();
}
此外,还可以在泛型约束中使用类型参数,例如:
function getProperty<T, K extends keyof T>(obj: T, key: K) {
return obj[key];
}
let x = { a: 1, b: 2, c: 3, d: 4 };
getProperty(x, "a"); // okay
getProperty(x, "m"); // error: Argument of type 'm' isn't assignable to 'a' | 'b' | 'c' | 'd'.
能够用一个类型参数的特征去约束另一个类型参数,相当强大
七.总结
之所以叫泛型,是因为能够作用于一系列类型,是在具体类型之上的一层抽象:
Generics are able to create a component that can work over a variety of types rather than a single one. This allows users to consume these components and use their own types.