TypeScript中的泛型
泛型:Generics
软件工程的主要部分就是构建一些即有声明良好且稳定的API又可重用的组件。而这些组件能帮助我们构建一个健壮且可扩展性强的系统。
在高级语言中,我们创造了一种可重用的组件,叫做泛型(Generics),用来处理不同类型的对象而并非单一类型的对象。
我们先来看一个例子,这个例子描述了一个函数,这个函数的作用是输入什么就输出什么。在不使用泛型的情况下,我们通常会这么声明这个函数。
示例:
function identity(arg: any): any {
return arg;
}
但是如果我们使用any类型,在我们调用这个方法获得返回值后我们就失去了这个输出结果的数据类型。我们如果输入一个数字(number),那么能得到的就只有any类型。
因此,我们需要使用类型捕捉的方式来进行类型的获取。这样我们在获取返回值时也可以获取到返回值的类型。在这里,我们使用一种叫做 类型变量 (type variable)的特殊变量。这种变量专门处理变量的类型而不是变量的值。
示例:
function identity<T>(arg: T): T {
return arg;
}
现在我们添加了一个类型变量T到函数中,这个变量允许我们获取用户在使用这个函数时所提供的变量类型。这个函数里我们将类型T作为参数类型和返回值类型。我们可以在一个函数里使用某一种类型作为参数类型,而返回值类型则是另外一种。此时,我们就可以称上面那个函数为泛型的。
我们现在用两种方式来调用这个函数。第一种就是我们将参数的类型和参数本身传给这个函数。需要注意的是类型参数需要用<>包裹。如:
示例:
let output = identity<string>("myString"); // type of output will be 'string'
同样,我们可以通过只传递参数来隐式调用函数,编译器会通过 类型参数推理 (type argument inference)的方式告知编译器我们使用了哪种类型作为类型T。
示例:
let output = identity("myString"); // type of output will be 'string'
这种方式看起来更易读易写,但是在因对复杂的情况时,请不要省略类型变量,以防止造成不必要的错误。
使用泛型类型变量:Working with Generic Type Variables
在你使用泛型时,编译器会强制要求你的函数体内每一个使用了泛型类型的变量的属性是准确的。
我们先来看一下例子。
示例:
function loggingIdentity<T>(arg: T): T {
console.log(arg.length); // Error: T doesn't have .length
return arg;
}
这个地方会报错,原因是T类型的数据并不一定包含length这个属性。再来看下一个例子。
示例:
function loggingIdentity<T>[](arg: T[]): T[] {
console.log(arg.length); // Array has a .length, so no more error
return arg;
}
此处没有错误,原因是一个数组是有长度的。如果我们希望传入的是一个T类型的数据且它有length属性,我们应该怎么做呢?下面我们来介绍泛型类型。
泛型类型:Generic Type
我们来看一下完整的泛型函数定义。
示例:
function identity<T>(arg: T): T {
return arg;
let myIdentity: <T>(arg: T) => T = identity;
我们可以在函数定义时修改函数中类型参数的参数名。
示例:
function identity<T>(arg: T): T {
return arg;
let myIdentity: <U>(arg: U) => U = identity;
同样,我们可以通过类似写对象的方式来写泛型的调用签名。(关于调用签名,我们可以回顾接口章节)。
示例:
function identity<T>(arg: T): T {
return arg;
let myIdentity: {<T>(arg: T): T} = identity;
此外,我们还可以定义一个描述泛型函数类型的接口。
示例:
interface GenericIdentityFn {
<T>(arg: T): T;
function identity<T>(arg: T): T {
return arg;
let myIdentity: GenericIdentityFn = identity;
更进一步,我们可以通过将泛型类型变量提取到接口描述中,使接口中所有的函数、属性都接受泛型类型的描述,使其成为一个泛型接口。
示例:
interface GenericIdentityFn<T> {
(arg: T): T;
function identity<T>(arg: T): T {
return arg;
let myIdentity: GenericIdentityFn<number> = identity;
在此时,我们使用接口来描述一个变量时,就要告知这个接口接收什么样的类型来描述泛型。
和描述一个泛型接口类似的,我们来描述一个泛型类。
示例:
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };
我们在讨论类的时候说到类其实有两个部分,一个是静态部分一个是实例部分。而泛型只覆盖了实例部分。而在使用类的静态部分时,我们无法使用泛型来描述它的静态部分。
泛型约束:Generic Constraints
我们现在来讨论一下泛型的兼容性。回到之前的一个例子。
示例:
function loggingIdentity<T>(arg: T): T {
console.log(arg.length); // Error: T doesn't have .length
return arg;
}
在这个例子中,我们需要访问arg的length属性,而编译器不能确保所有的类型全都有这个属性,所以它会报错。而通常我们在使用这个函数时,我们期望的并不是所有的类型,而是带有length属性的类型。所以如果一个变量有一些成员,且某些成员是必须的,我们就需要列出需求清单来约束传入的类型。
为了解决这个问题,我们通过声明一个接口来描述我们的约束。在这里,我们创建一个带有length属性的接口,并使类型T扩展它来作为约束。
示例:
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({length: 10, value: 3});
这样就意味着在做类型检查时,我们会对参数进行检查,看看这个类型是否符合接口的描述——即传入的对象必须有一个实例部分的属性,名字叫做length且类型是number。如果调用的参数不符合这个约束,则编译器会报错。
同样我们可以使用泛型来扩展泛型。
示例:
function copyFields<T extends U, U>(target: T, source: U): T {
for (let id in source) {
target[id] = source[id];
return target;
let x = { a: 1, b: 2, c: 3, d: 4 };
copyFields(x, { b: 10, d: 20 }); // okay
copyFields(x, { Q: 90 }); // error: property 'Q' isn't declared in 'x'.
这个描述表示的是,我们接受两个泛型类型的参数用作函数的参数,而第一个类型要被第二个类型所约束,即第二个类型的对象属性必须存在于第一个类型的对象属性列表中。否则就会报错。我们可以将第一个类型看成是子类而第二个类型看成是父类,但是要求并不如继承那样严格罢了。
在泛型中使用类类型:Using Class Types in Generics
我们可以讲一个类作为类型传入到泛型声明的函数中。所以我们需要对其做一个约束:我们判断传入的类型是否存在一个new的函数,且这个函数返回一个该类型的对象。
示例:
function create<T>(c: {new(): T; }): T {
return new c();
}
这个代码分为几个部分。接受的参数为c: {new(): T; },表示传入的参数名为c,它有一个名为new()的属性(这个属性恰好就是构造函数),且这个属性的返回值为T,这就决定了传入的c是类类型,即类的类型而不是对象,作为一个参数传入函数中。如果要理解这个模型我们可以借助接口章节的范例来理解。其次,我们定义了返回类型为T的返回值,而在函数体内,我们通过new来新建类型c的对象。
这和接口章节的范例有些类似。此处我们通过泛型这种方式创建的是任意一种类型的对象*(此处可以考证一下如果我们对new()的参数列表进行约束是否会影响传入的类)*。而使用接口描述一个符合构造函数约束的类,然后用接口作为类型检查的标准来检查这个传入的类是否实现了该接口。区别就在于这个类是否是一个接口的实现。
示例:
class BeeKeeper {
hasMask: boolean;
class ZooKeeper {
nametag: string;
class Animal {
numLegs: number;
class Bee extends Animal {
keeper: BeeKeeper;
class Lion extends Animal {