泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。

不用泛型的示例,传入什么值返回什么值:

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

也可使用 any,但是却会丢失类型信息

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

使用泛型:

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

不同于使用 any,泛型不会丢失信息,像第一个例子那像保持准确性,传入数值类型并返回数值类型。

定义了泛型函数后,可以用两种方法使用。

第一种是,传入所有的参数,包含类型参数:

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

这里我们明确的指定了T是string类型,并做为一个参数传给函数,使用了<>括起来而不是()。

第二种方法更普遍。利用了类型推论 — 即编译器会根据传入的参数自动地帮助我们确定T的类型:

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

注意我们没必要使用尖括号(<>)来明确地传入类型;编译器可以查看myString的值,然后把T设置为它的类型。 类型推论帮助我们保持代码精简和高可读性。如果编译器不能够自动地推断出类型的话,只能像上面那样明确的传入T的类型,在一些复杂的情况下,这是可能出现的。

泛型方法

先看一个简单的示例:

  1. function createArray<T>(length: number, value: T): Array<T> {
  2. let result: T[] = [];
  3. for (let i = 0; i < length; i++) {
  4. result[i] = value;
  5. }
  6. return result;
  7. }
  8. createArray<string>(3, 'x'); // ['x', 'x', 'x']

上例中,我们在函数名后添加了 <T>,其中 T 用来指代任意输入的类型,在后面的输入 value: T 和输出 Array<T> 中即可使用了。

接着在调用的时候,可以指定它具体的类型为 string。当然,也可以不手动指定,而让类型推论自动推算出来:

  1. function createArray<T>(length: number, value: T): Array<T> {
  2. let result: T[] = [];
  3. for (let i = 0; i < length; i++) {
  4. result[i] = value;
  5. }
  6. return result;
  7. }
  8. createArray(3, 'x'); // ['x', 'x', 'x']

接收一个数组

可以指定传入的参数是一个 Array,这样就可以使用 Array 的一些属性和方法:

  1. function loggingIdentity<T>(arg: T[]): T[] {
  2. console.log(arg.length);
  3. console.log(arg.join());
  4. return arg;
  5. }

也可写成:

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

多个类型参数

定义泛型的时候,可以一次定义多个类型参数:

  1. function swap<T, U>(tuple: [T, U]): [U, T] {
  2. return [tuple[1], tuple[0]];
  3. }
  4. swap([7, 'seven']); // ['seven', 7]

上例中,我们定义了一个 swap 函数,用来交换输入的元组。

泛型参数的默认类型

在 TypeScript 2.3 以后,我们可以为泛型中的类型参数指定默认类型。当使用泛型时没有在代码中直接指定类型参数,从实际值参数中也无法推测出时,这个默认类型就会起作用。

  1. function createArray<T = string>(length: number, value: T): Array<T> {
  2. let result: T[] = [];
  3. for (let i = 0; i < length; i++) {
  4. result[i] = value;
  5. }
  6. return result;
  7. }

泛型接口

有以下泛型定义的方法:

  1. let myIdentity: <U>(arg: U) => U = function <D>(arg: D): D {
  2. return arg;
  3. };
  4. // or
  5. let myIdentity: <U>(arg: U) => U = <D>(arg: D): D => {
  6. return arg;
  7. };

改为接口约束的形似:

  1. interface GenericIdentityFn {
  2. <T>(arg: T): T;
  3. }
  4. let myIdentity: GenericIdentityFn = function <T>(arg: T): T {
  5. return arg;
  6. };
  7. // or
  8. let myIdentity: GenericIdentityFn = <T>(arg: T): T => {
  9. return arg;
  10. };

其中T只是一个泛型的形式标识,也可写为任意其他标识(U、D等)

我们可以把泛型参数提前到接口名上。 这样我们就能清楚的知道使用的具体是哪个泛型类型(比如: Dictionary<string>而不只是Dictionary)。这样接口里的其它成员也能知道这个参数的类型了:

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

一个简单的示例:

  1. interface CreateArrayFunc {
  2. <T>(length: number, value: T): Array<T>;
  3. }
  4. let createArray: CreateArrayFunc;
  5. createArray = function<T>(length: number, value: T): Array<T> {
  6. let result: T[] = [];
  7. for (let i = 0; i < length; i++) {
  8. result[i] = value;
  9. }
  10. return result;
  11. }
  12. createArray(3, 'x'); // ['x', 'x', 'x']

或者:

  1. interface CreateArrayFunc<T> {
  2. (length: number, value: T): Array<T>;
  3. }
  4. let createArray: CreateArrayFunc<any>;
  5. createArray = function<T>(length: number, value: T): Array<T> {
  6. let result: T[] = [];
  7. for (let i = 0; i < length; i++) {
  8. result[i] = value;
  9. }
  10. return result;
  11. }
  12. createArray(3, 'x'); // ['x', 'x', 'x']

泛型类

与泛型接口类似,泛型也可以用于类的类型定义中:

  1. class Generic<T> {
  2. zeroValue: T;
  3. add: (x: T, y: T) => T;
  4. }
  5. let numberGeneric = new Generic<number>();
  6. numberGeneric.zeroValue = 0;
  7. numberGeneric.add = function (x, y) { return x + y; };
  8. console.log(numberGeneric.add(1, 2)) // 3
  9. let stringrGeneric = new Generic<string>();
  10. stringrGeneric.zeroValue = "";
  11. stringrGeneric.add = function(x, y) { return x + y; };
  12. console.log(stringrGeneric.add('Hello ', 'world')) // Hello world

泛型约束

之前的一个例子,我们有时候想操作某类型的一组值,并且我们知道这组值具有什么样的属性。 在loggingIdentity例子中,我们想访问arglength属性,但是编译器并不能证明每种类型都有length属性,所以就报错了。

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

相比于操作any所有类型,我们想要限制函数去处理任意带有.length属性的所有类型。 只要传入的类型有这个属性,我们就允许,就是说至少包含这一属性。 为此,我们需要列出对于T的约束要求。

为此,我们定义一个接口来描述约束条件。 创建一个包含 .length属性的接口,使用这个接口和extends关键字来实现约束:

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

现在这个泛型函数被定义了约束,因此它不再是适用于任意类型:

  1. loggingIdentity(3); // Error, number doesn't have a .length property

我们需要传入符合约束类型的值,必须包含必须的属性:

  1. loggingIdentity([])
  2. loggingIdentity('')
  3. loggingIdentity({length: 10, value: 3});

多个类型参数之间也可以互相约束:

  1. function copyFields<T extends U, U>(target: T, source: U): T {
  2. for (let id in source) {
  3. target[id] = (<T>source)[id];
  4. }
  5. return target;
  6. }
  7. let x = { a: 1, b: 2, c: 3, d: 4 };
  8. copyFields(x, { b: 10, d: 20 });

上例中,我们使用了两个类型参数,其中要求 T 继承 U,这样就保证了 U 上不会出现 T 中不存在的字段。