• 泛型
    • 泛型的Hello World
    • 泛型类型变量
    • 泛型类型
    • 泛型类
    • 限制泛型
      • 在泛型限制中使用类型参数
      • 在泛型中使用类

    泛型

    软件开发的一个主要部分就是不仅仅只构建具有一成不变API的组件,更是具有可复用性的组件。不仅仅兼容当前数据类型,还兼容日后数据类型的组件,将使构建大型系统更为轻松。

    C#Java中,构建可复用组件的主要武器就是“泛型”,它让一个组件可以兼容许多类型的对象,使得用户可以使用自己的类型来运行组件。

    泛型的Hello World

    我们以一个“Hello World”泛型开始。这个函数会返回传入的参数。你可以把它当成echo命令的复刻。

    如果没有泛型,那么我们只能明确地指定参数的类型:

    1. function identity(arg: number): number {
    2. return arg;
    3. }

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

    1. function identity(arg: any): any {
    2. return arg;
    3. }

    使用any的确可以达到泛型的效果,但是这样做的话,我们就丢失了返回值的类型。如果我们传入一个数字,但返回值的依然是any类型。

    为了能获取到返回值的类型,我们需要有一种方式来捕获参数的类型。这里,我们使用一个类型变量,这是一种特殊的不表示值而是表示类型的变量。

    1. function identity<T>(arg: T): T {
    2. return arg;
    3. }

    现在,我们添加了一个类型变量T。这使得我们可以捕获到用户传递的参数类型(如number)。例子中,我们使之作为了返回值类型。这样,我们就做到了返回值类型和用户传入的参数类型一致。

    我们将这个版本的identity函数称作一个泛型函数。与使用any类型不同,这个函数与第一个指定参数和返回值类型为number的版本一样精确。

    一旦我们定义了泛型函数identity,我们有两种办法调用它。第一种就是传入所有参数,包括类型参数:

    1. var output = identity<string>("myString"); // type of output will be 'string'

    上述例子中,我们在<>中传入string,来明确地将T设置为字符串。

    第二种方式更普遍一些。我们使用“类型参数推导”,即我们让编译器基于我们传入的参数类型,自动地为我们设置变量T

    1. var output = identity("myString"); // type of output will be 'string'

    注意,我们并没有明确将类型传入<>中,编译器会查找"myString"值的类型,然后将它赋值给T。这有助于减少代码量。但当例子更为复杂时,“类型参数推导”可能会失败,届时你需要使用第一种方式,明确地指定T的值。

    泛型类型变量

    当你开始使用泛型时,你会注意到当你创建如Identity这样的函数后,编译器为了保证函数的正确性,它只会允许你进行一些所有类型都共有的操作。因为,它可能是任何的类型。

    让我们继续以identity函数为例子:

    1. function identity<T>(arg: T): T {
    2. return arg;
    3. }

    如果我们想在控制台打印出arglength属性,我们可能会这么写:

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

    当我们这么做后,编译器会给我们一个报错,因为并不能确保arg一定是一个包含多个成员的类型。你可能传递一个number类型的参数,而它并没有length属性。

    如果要明确地表示参数是一个数组,那么我们应该直接将参数描述为T的数组。这样,它就一定会有length属性:

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

    我们也可以这么写:

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

    如果你使用过其他一些编程语言,那么你可能已经对这样的语法十分熟悉了。在下一章中,我们将展示如果创建如Array<T>这样的泛型类型。

    泛型类型

    在上一节里,我们创建了泛型函数identity,它可以搭配许多不同类型的参数。在这一节里,我们将会探索函数类型本身,以及如何创建泛型接口。

    泛型函数类型与普通函数类型相似,只是将参数类型置于最前面:

    1. function identity<T>(arg: T): T {
    2. return arg;
    3. }
    4. var myIdentity: <T>(arg: T)=>T = identity;

    我们也可以使用不同泛型类型参数名:

    1. function identity<T>(arg: T): T {
    2. return arg;
    3. }
    4. var myIdentity: <U>(arg: U)=>U = identity;

    我们还可以将泛型类型作为一个对象类型书写:

    1. function identity<T>(arg: T): T {
    2. return arg;
    3. }
    4. var myIdentity: {<T>(arg: T): T} = identity;

    下面我来看看泛型接口,我们将上述例子中的类型写成一个泛型接口:

    1. interface GenericIdentityFn {
    2. <T>(arg: T): T;
    3. }
    4. function identity<T>(arg: T): T {
    5. return arg;
    6. }
    7. var myIdentity: GenericIdentityFn = identity;

    我们可能希望将函数的类型参数变成整个接口的类型参数。这使得该类型参数对接口的所有成员可见。

    1. interface GenericIdentityFn<T> {
    2. (arg: T): T;
    3. }
    4. function identity<T>(arg: T): T {
    5. return arg;
    6. }
    7. var myIdentity: GenericIdentityFn<number> = identity;

    注意上述例子中的变化。我们不再描述一个泛型函数,而是将之作为了泛型类型的一部分。当我们使用GenericIdentityFn时,我们需要明确地指明参数类型(例子中是number)。理解何时将类型参数放在函数,以及何时放在接口上,将会有助于你理解类型的哪一部分是需要进行泛型的。

    除了泛型接口,我们还可以创建泛型类。但是,不能创建泛型枚举值和泛型模块。

    泛型类

    泛型类与泛型接口类似,泛型类需要在它的名字后的<>中,添加类型参数:

    1. class GenericNumber<T> {
    2. zeroValue: T;
    3. add: (x: T, y: T) => T;
    4. }
    5. var myGenericNumber = new GenericNumber<number>();
    6. myGenericNumber.zeroValue = 0;
    7. myGenericNumber.add = function(x, y) { return x + y; };

    这是一个非常宽泛的GenericNumber类,你可能留意到,除了number类型外,我们还可以传递其他的如字符串或更复杂的对象:

    1. var stringNumeric = new GenericNumber<string>();
    2. stringNumeric.zeroValue = "";
    3. stringNumeric.add = function(x, y) { return x + y; };
    4. alert(stringNumeric.add(stringNumeric.zeroValue, "test"));

    与接口一样,将类型参数置于类级别,可以保证所有类成员得到的都是相同的类型。

    如我们在讨论类时提到的,一个类有静态部分和实例部分。泛型类只会影响它的实例部分。所以在使用时,静态部分将不能使用泛型类的类型参数。

    限制泛型

    回想一下之前的一个例子,我们想要取得arg参数的.length属性,但是编译器并不能保证它有一个.length属性,所以它警告我们不能做这样的假设:

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

    我们想要将此泛型函数限制为只能传入具有.length属性的参数。只要参数有这个属性,那么我就允许它被传入。为了能这么做,我们需要限制T

    我们将创建一个接口来描述我们的限制,这里,我们创建一个只有.length属性的接口,然后我们使用etends关键字,让T继承这个接口,来实现我们的限制:

    1. interface Lengthwise {
    2. length: number;
    3. }
    4. function loggingIdentity<T extends Lengthwise>(arg: T): T {
    5. console.log(arg.length); // Now we know it has a .length property, so no more error
    6. return arg;
    7. }

    这样以后,我们的泛型函数就是受限制的了,它将不再允许传入没有.length属性的参数:

    1. loggingIdentity(3); // Error, number doesn't have a .length property
    2. loggingIdentity({length: 10, value: 3});

    在泛型限制中使用类型参数

    在一些情况下,声明一个限制另一个类型参数的类型参数是有很有用的,例子:

    1. function find<T, U extends Findable<T>>(n: T, s: U) { // errors because type parameter used in constraint
    2. // ...
    3. }
    4. find (giraffe, myAnimals);

    你也可以直接替换掉泛型限制:

    1. function find<T>(n: T, s: Findable<T>) {
    2. // ...
    3. }
    4. find(giraffe, myAnimals);

    注意:上面两个函数并不是完全相同的。第一个函数的返回值可以是U类型的,而第二个函数则做不到。

    在泛型中使用类

    TypeScript中创建工厂函数时,你可能需要将类作为参数,例子:

    1. function create<T>(c: {new(): T; }): T {
    2. return new c();
    3. }

    一个更高级的例子是使用prototype属性来推导类的关系:

    1. class BeeKeeper {
    2. hasMask: boolean;
    3. }
    4. class ZooKeeper {
    5. nametag: string;
    6. }
    7. class Animal {
    8. numLegs: number;
    9. }
    10. class Bee extends Animal {
    11. keeper: BeeKeeper;
    12. }
    13. class Lion extends Animal {
    14. keeper: ZooKeeper;
    15. }
    16. function findKeeper<A extends Animal, K> (a: {new(): A;
    17. prototype: {keeper: K}}): K {
    18. return a.prototype.keeper;
    19. }
    20. findKeeper(Lion).nametag; // typechecks!