泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。
不用泛型的示例,传入什么值返回什么值:
function identity(arg: number): number {
return arg;
}
也可使用 any,但是却会丢失类型信息
function identity(arg: any): any {
return arg;
}
使用泛型:
function identity<T>(arg: T): T {
return arg;
}
不同于使用 any,泛型不会丢失信息,像第一个例子那像保持准确性,传入数值类型并返回数值类型。
定义了泛型函数后,可以用两种方法使用。
第一种是,传入所有的参数,包含类型参数:
let output = identity<string>("myString"); // type of output will be 'string'
这里我们明确的指定了T是string类型,并做为一个参数传给函数,使用了<>括起来而不是()。
第二种方法更普遍。利用了类型推论 — 即编译器会根据传入的参数自动地帮助我们确定T的类型:
let output = identity("myString"); // type of output will be 'string'
注意我们没必要使用尖括号(<>)来明确地传入类型;编译器可以查看myString的值,然后把T设置为它的类型。 类型推论帮助我们保持代码精简和高可读性。如果编译器不能够自动地推断出类型的话,只能像上面那样明确的传入T的类型,在一些复杂的情况下,这是可能出现的。
泛型方法
先看一个简单的示例:
function createArray<T>(length: number, value: T): Array<T> {
let result: T[] = [];
for (let i = 0; i < length; i++) {
result[i] = value;
}
return result;
}
createArray<string>(3, 'x'); // ['x', 'x', 'x']
上例中,我们在函数名后添加了 <T>
,其中 T
用来指代任意输入的类型,在后面的输入 value: T
和输出 Array<T>
中即可使用了。
接着在调用的时候,可以指定它具体的类型为 string
。当然,也可以不手动指定,而让类型推论自动推算出来:
function createArray<T>(length: number, value: T): Array<T> {
let result: T[] = [];
for (let i = 0; i < length; i++) {
result[i] = value;
}
return result;
}
createArray(3, 'x'); // ['x', 'x', 'x']
接收一个数组
可以指定传入的参数是一个 Array,这样就可以使用 Array 的一些属性和方法:
function loggingIdentity<T>(arg: T[]): T[] {
console.log(arg.length);
console.log(arg.join());
return arg;
}
也可写成:
function loggingIdentity<T>(arg: Array<T>): Array<T> {
console.log(arg.length);
console.log(arg.join());
return arg;
}
多个类型参数
定义泛型的时候,可以一次定义多个类型参数:
function swap<T, U>(tuple: [T, U]): [U, T] {
return [tuple[1], tuple[0]];
}
swap([7, 'seven']); // ['seven', 7]
上例中,我们定义了一个 swap
函数,用来交换输入的元组。
泛型参数的默认类型
在 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;
}
泛型接口
有以下泛型定义的方法:
let myIdentity: <U>(arg: U) => U = function <D>(arg: D): D {
return arg;
};
// or
let myIdentity: <U>(arg: U) => U = <D>(arg: D): D => {
return arg;
};
改为接口约束的形似:
interface GenericIdentityFn {
<T>(arg: T): T;
}
let myIdentity: GenericIdentityFn = function <T>(arg: T): T {
return arg;
};
// or
let myIdentity: GenericIdentityFn = <T>(arg: T): T => {
return arg;
};
其中T只是一个泛型的形式标识,也可写为任意其他标识(U、D等)
我们可以把泛型参数提前到接口名上。 这样我们就能清楚的知道使用的具体是哪个泛型类型(比如: Dictionary<string>
而不只是Dictionary
)。这样接口里的其它成员也能知道这个参数的类型了:
interface GenericIdentityFn<T> {
(arg: T): T;
}
let myIdentity: GenericIdentityFn<any> = <T>(arg: T): T => {
return arg;
};
一个简单的示例:
interface CreateArrayFunc {
<T>(length: number, value: T): Array<T>;
}
let createArray: CreateArrayFunc;
createArray = function<T>(length: number, value: T): Array<T> {
let result: T[] = [];
for (let i = 0; i < length; i++) {
result[i] = value;
}
return result;
}
createArray(3, 'x'); // ['x', 'x', 'x']
或者:
interface CreateArrayFunc<T> {
(length: number, value: T): Array<T>;
}
let createArray: CreateArrayFunc<any>;
createArray = function<T>(length: number, value: T): Array<T> {
let result: T[] = [];
for (let i = 0; i < length; i++) {
result[i] = value;
}
return result;
}
createArray(3, 'x'); // ['x', 'x', 'x']
泛型类
与泛型接口类似,泛型也可以用于类的类型定义中:
class Generic<T> {
zeroValue: T;
add: (x: T, y: T) => T;
}
let numberGeneric = new Generic<number>();
numberGeneric.zeroValue = 0;
numberGeneric.add = function (x, y) { return x + y; };
console.log(numberGeneric.add(1, 2)) // 3
let stringrGeneric = new Generic<string>();
stringrGeneric.zeroValue = "";
stringrGeneric.add = function(x, y) { return x + y; };
console.log(stringrGeneric.add('Hello ', 'world')) // Hello world
泛型约束
之前的一个例子,我们有时候想操作某类型的一组值,并且我们知道这组值具有什么样的属性。 在loggingIdentity
例子中,我们想访问arg
的length
属性,但是编译器并不能证明每种类型都有length
属性,所以就报错了。
function loggingIdentity<T>(arg: T): T {
console.log(arg.length); // Error: T doesn't have .length
return arg;
}
相比于操作any所有类型,我们想要限制函数去处理任意带有.length
属性的所有类型。 只要传入的类型有这个属性,我们就允许,就是说至少包含这一属性。 为此,我们需要列出对于T的约束要求。
为此,我们定义一个接口来描述约束条件。 创建一个包含 .length
属性的接口,使用这个接口和extends
关键字来实现约束:
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;
}
现在这个泛型函数被定义了约束,因此它不再是适用于任意类型:
loggingIdentity(3); // Error, number doesn't have a .length property
我们需要传入符合约束类型的值,必须包含必须的属性:
loggingIdentity([])
loggingIdentity('')
loggingIdentity({length: 10, value: 3});
多个类型参数之间也可以互相约束:
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 中不存在的字段。