1. 基本示例
  2. 使用泛型变量
  3. 泛型类型
  4. 泛型类
  5. 泛型约束

1. 基本示例

考虑到组件的可重用性,引入了泛型的概念,可以使得函数的返回值类型总是和传入值类型保持一致,而不管你传入的是什么具体的类型,使用了泛型的函数叫做泛型函数。

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

调用该函数时,通常依赖编译器自身的类型推断来确定 T 的类型:

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

但复杂情况下,或许还是需要手动使用 <>尖括号来明确传入类型:

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

2. 使用泛型变量

需要时可以把泛型变量 T 当做类型的一部分使用,就像其他的类型变量一样,number[]string[]

  1. // T[] 表示元素类型是 T 的数组
  2. function identity<T>(arg: T[]): T[] {
  3. console.log(arg.length); // 通过声明泛型 T[],进一步缩小范围,获取 length 属性时才不会报错
  4. return arg;
  5. }

使用过其它语言的话,也可以这样来声明泛型函数:

  1. function identity<T>(arg: Array<T>): Array<T> {
  2. console.log(arg.length);
  3. return arg;
  4. }

3. 泛型类型

刚才已经创建了一个泛型函数,它可以灵活地根据具体传入的类型,而返回相同的类型。现在继续研究一下函数本身是什么类型,之前接口那一篇中提到了函数这种类型的接口,用来把函数的类型抽象出来、给出定义,其实就是在接口中定义了函数的签名:

  1. interface SearchFunc {
  2. (source: string, subString: string): boolean;
  3. }

下面定义了 myIdentity 变量,并且指明了它的类型,是一种泛型函数,接下来用它存储已定义好的泛型函数,这没问题:

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

还可以使用带有调用签名的对象字面量来定义泛型函数:

  1. let myIdentity: {<T>(arg: T): T} = identity;

这种花括号包裹着函数签名的对象字面量写法,其实就可以单独抽出来,定义为一个泛型接口了,以便多次复用。所谓的泛型接口,也就是当函数接口加入了泛型时的称呼,然后改写刚才的 myIdentity 变量声明,用泛型接口代替了直接写死:

  1. interface GenericIdentityFn {
  2. <T>(arg: T): T
  3. }
  4. let myIdentity: GenericIdentityFn = identity;

到目前为止,其实和最开始的泛型例子区别不大,只是抽出了个所谓的泛型接口,泛型函数内部的 T 究竟是什么具体的类型,还是要等到真正调用函数的时候才能确定。可是如果我们想早点确定下来呢?换句话说,既然已经抽离出来了泛型接口,我们想把 T 的指定权从函数调用处,转移到泛型接口使用处。这样还有个好处是接口中的其他成员也能利用 T 了。

很简单,类似于给一个函数定义形参一样,下面把这个 T 也当做接口的“形参”,刚才篇幅可能有点长,完整写法及用法如下:

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

可以看到,指定 T 具体是什么类型的时机发生在使用接口的时候而非等到调用函数的时候。何时把参数放在调用签名里和何时放在接口上,取决于具体实践中,想把哪部分类型归属到泛型部分。

4. 泛型类

除了泛型接口,还可以创建泛型类,和泛型接口差不多。与接口一样,直接把泛型类型放在类后面:

  1. class GenericNumber<T> {
  2. zeroValue: T;
  3. add: (x: T, y: T) => T;
  4. }
  5. let myGenericNumber = new GenericNumber<number>();
  6. myGenericNumber.zeroValue = 0;
  7. myGenericNumber.add = function (x, y) { return x + y; };
  8. let result1 = myGenericNumber.add(myGenericNumber.zeroValue, 200);
  9. console.log(result); // 200

既然是泛型,所以并不限制只能使用 number 类型,也可以用 string 或更复杂的类型:

  1. let stringNumeric = new GenericNumber<string>();
  2. stringNumeric.zeroValue = "";
  3. stringNumeric.add = function(x, y) { return x + y; };
  4. let result2 = stringNumeric.add(stringNumeric.zeroValue, 'test');
  5. console.log(test); // 'test'

5. 泛型约束

5.1 基本用法

在本篇最初的例子中说了为什么需要泛型,以及更“精确”一点的泛型 T[],为了能正确推断出 T 具有 length 属性。再看一下本篇最初的例子:

  1. function identity<T>(arg: T[]): T[] {
  2. console.log(arg.length); // 通过声明泛型 T[],进一步缩小范围,获取 length 属性时才不会报错
  3. return arg;
  4. }

但如果只是为了满足参数具有 length 属性这一个要求,却不得不传入定义为 T[],显然这样丧失了一些泛型的灵活性和精髓。同样类似的需求,在定义泛型时能否既拥有必要的约束,又不至于太死板?这就引入了泛型约束的概念:

  1. interface Lengthwise {
  2. length: number;
  3. }
  4. function loggingIdentity<T extends Lengthwise>(arg: T): T {
  5. console.log(arg.length); // 现在我们知道 arg 拥有 length 属性
  6. return arg;
  7. }

调用该泛型函数时,只要传入的参数类型符合 Lengthwise 接口即可:

  1. loggingIdentity(3); // 报错,数字 3 没有 length 属性
  2. loggingIdentity({ length: 10, value: 5 }); // ok,传入的对象有 length 属性
  3. loggingIdentity([1, 2, 3]); // 也 ok

5.2 在泛型约束中使用类型参数

可以声明一个类型参数,它被另一个类型参数所约束。比如用属性名获取对象属性时,想要保证该属性是存在于对象上的:

  1. function getProperty<T, K extends keyof T>(obj: T, key: K) {
  2. return obj[key];
  3. }
  4. let x = { a: 1, b: 2, c: 3, d: 4 };
  5. getProperty(x, 'a');
  6. getProperty(x, 'c');
  7. getProperty(x, 'e'); // 报错

5.3 在泛型里使用类类型

之前在接口那一节讲类类型时,也演示了工厂函数相关的操作。 现在结合泛型在创建工厂函数时,并不写死传入的构造函数具体类型,而是引用构造函数的类类型,满足构造函数类型的类,就可以在调用工厂函数时传入其中:

  1. class Person {
  2. name: string = 'Tom';
  3. print() {
  4. console.log(this.name);
  5. }
  6. }
  7. function create<T>(c: { new(): T; }): T {
  8. return new c();
  9. }
  10. let p = create(Person);
  11. p.print(); // 'Tom'

一个更高级的例子,刚才的工厂函数可以进一步使用原型属性推断并约束构造函数与类实例的关系:

  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 createInstance<A extends Animal>(c: new () => A): A {
  17. return new c();
  18. }
  19. createInstance(Lion).keeper.nametag; // 可以成功推断
  20. createInstance(Bee).keeper.hasMask; // 可以成功推断
  21. createInstance(Animal).numLegs; // 可以成功推断

点到为止,实际写项目时可能才有更深入的理解和摸索出最佳实践。