0%

6.泛型

简介

软件工程活动中的一个主要方面是构建组件,这些组件不仅仅是有定义好的API,也要是可复用的。那些对于今天的数据是可用的,对于将来的数据也是可用的组件,将在构建大型软件系统时给你最大的灵活性。
在C#或Java这样的语言中,一个主要的功能就是用泛型来创建可复用的组件。泛型表示这些组件可用在许多不同的数据类型上,尔不是单一的数据类型。这就允许用户在这些组件里面使用自己的数据类型。

Hello World

让我们写一个泛型版的Hello World: ID 函数。ID函数指的是一种你给它什么它就返回给你什么的函数,就像echo命令一样。

function identity(arg: number): number {
    return arg;
}

或者,我们可以使用any类型:

function identity(arg: any): any {
    return arg;
}

这里实际上该用泛型,但是用了any,这会导致函数返回any 类型而丢失掉了类型信息。例如,传入了一个number类型,对于返回的类型却只知道它是any(注:从而失去了类型检查和进一步的类型推导)。

所以,我们需要一种捕获类型的方法,然后我们可用这种方法了标示返回类型。在这里,我们使用类型变量——一种特殊的变量为类型而生而不是为值而生。

function identity<T>(arg:T){
    return arg
}

现在我们给Id函数加入了类型变量T。这个T让我们可以捕获用户提供的类型(例如number),所以我们就可以使用这个类型。在这里,我们把T用做返回值类型。

我们说这个版本的id函数是个泛型函数,应为它可以用在很多类型上。和用any不同,它和number类型的哪个id函数一样是精准的(如,不会丢失类型信息)。

一旦我们写好了泛型函数,我们有两种调用它的方式。第一种是传递所有的参数给它,包括类型参数:

let output = identity<string>("myOutput")

这里,我们明确的指定了T为string,其作为调用函数的一个类型参数,需用用尖括号括起来而不是圆括号。
第二种方式更为通用,即使用类型推断,即我们让编译器根据我们传入的类型自行设置T的值。

let output = identity("myString")

注意,我们没有用尖括号来明确指明类型参数,编译器根据"myString"来决定T的值。尽管类型参数的自动推断可以使代码更短,可读性更强,但是有些复杂的情况下编译器不能推断出泛型的类型,这时就需要你明确指定泛型的类型。

使用泛型类型变量

当你使用泛型时你会注意到当你创建像identity这样的泛型函数的时候,编译器会确保你在函数体内正确的使用了泛型。即,你实际应该把看做是所有类型。
如果我们想在函数体内把arglength输出出来,会发生什么呢? 我们可能会这样写:

function loggingIdentity<T>(arg:T):T{
    console.log(arg.length);
    return arg;
}

如果我们这样做,编译器会报告一个错误,即我们使用了arglength成员,但是我们没说过arg有这样一个成员。上面说过,你应该把T看成是所有类型,所以可能传进来的arg是一个数字,那么它是没有length属性的。

这里我们实际上是希望这个函数接受T数组而不是T。如果argT的数组,那么就可以访问.length属性了。

function loggingIdentity<T>(arg: T[]): T[] {
    console.log(arg.length);  // Array has a .length, so no more error
    return arg;
}

现在你可以把这个函数解读为:一个拥有类型参数T以及函数参数arg—— 为数组T类型 ——并返回T数组类型的泛型函数。如果我们传入一个数字数组,我们会得到一个返回的数字数组,那么Tnumber
这让我们可以把泛型类型T作为所有我们用到的类型的一个部分,而不是所有类型。这样给我们更多的灵活性。
对上面的例子我们也可使用下面的形式:

function loggingIdentity<T>(arg: Array<T>): Array<T> {
    console.log(arg.length);  // Array has a .length, so no more error
    return arg;
}

你也许很熟悉这种写法,在下节中,我们将会讨论如何创建类似于Array<T>这样的泛型。

泛形类型

在前面几个小节中我们创建了一个泛型函数,它可又接受一些类型,在这节中我们将探索函数自身的类型以及如何创建泛型接口。
泛型函数的类型和非泛型函数的类型是类似的,需把泛型参数写在最前面。

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; };
let stringNumeric = new GenericNumber<string>();
stringNumeric.zeroValue = "";
stringNumeric.add = function(x, y) { return x + y; };

alert(stringNumeric.add(stringNumeric.zeroValue, "test"));

就像使用接口一样,在类上使用泛型参数可让类中的的属性或方法可以使用这个泛型类型。

这章描述过,类的类型有两种方面:类方面和实例方面。泛型类的泛型只在其实例中可用。也就是说,静态方法不能访问泛型类的浮现参数。

对泛型的约束

如果你还记得前面的例子,你也许希望你的泛型函数中的泛型类型只实用于一部分的类型。对于loggingIdentity这个例子中,我们希望能访问arg的length属性,但是编译器无法保证每个类型都有一个length属性,所以它会给出一个警告。

function loggingIdentity<T>(arg: T): T {
    console.log(arg.length);  // Error: T doesn't have .length
    return arg;
}

除了使用any类型或所有类型,我们希望这个函数使用那些带有.length属性的类型。只要某类型有.length属性,我们就允许它被传递到函数中,而且只是要求这个属性是必须要有的。为了做到这点,我们必须对T作一些限定。

为了对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

除非我们传递一个有length属性的东西给它。

loggingIdentity({length: 10, value: 3});

在类型参数中使用限定

你可用一个类型参数来限定另一个类型参数。例如,我们想从一个给定的名字来访问一个对象的属性。我们希望能确保我们不会意外的访问了那些不存在的属性。所以我们在这两个泛型类型中应用一个限定。

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'.

在泛型中使用类类型

When creating factories in TypeScript using generics, it is necessary to refer to class types by their constructor functions. For example,
当我们在工厂函数中使用泛型的时候,在其构造函数中引用类类型是有必要的。例如:

function create<T>(c: {new(): T; }): T {
    return new c();
}

下面的例子演示了使用原型来引用和限定构造函数和类实例之间的关系。

class BeeKeeper {
    hasMask: boolean;
}

class ZooKeeper {
    nametag: string;
}

class Animal {
    numLegs: number;
}

class Bee extends Animal {
    keeper: BeeKeeper;
}

class Lion extends Animal {
    keeper: ZooKeeper;
}

function createInstance<A extends Animal>(c: new () => A): A {
    return new c();
}

createInstance(Lion).keeper.nametag;  // typechecks!
createInstance(Bee).keeper.hasMask;   // typechecks!