软件工程中,我们不仅要创建一致的定义良好的API,同时也要考虑可重用性。 组件不仅能够支持当前的数据类型,同时也能支持未来的数据类型,这在创建大型系统时为你提供了十分灵活的功能。
在像C#和Java这样的语言中,可以使用泛型来创建可重用的组件,一个组件可以支持多种类型的数据。 这样用户就可以以自己的数据类型来使用组件。

泛型之Hello World

下面来创建第一个使用泛型的例子:identity函数。 这个函数会返回任何传入它的值。 你可以把这个函数当成是 echo命令。
不用泛型的话,这个函数可能是下面这样:

  1. function identity(arg: number): number {
  2. return arg;
  3. }
  4. // 或者
  5. function identity(arg: any): any {
  6. return arg;
  7. }

使用any类型会导致这个函数可以接收任何类型的arg参数,这样就丢失了一些信息:传入的类型与返回的类型应该是相同的。如果我们传入一个数字,我们只知道任何类型的值都有可能被返回。
因此,我们需要一种方法使返回值的类型与传入参数的类型是相同的。 这里,我们使用了 类型变量,它是一种特殊的变量,只用于表示类型而不是值。

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

我们给identity添加了类型变量T。 T帮助我们捕获用户传入的类型(比如:number),之后我们就可以使用这个类型。 之后我们再次使用了 T当做返回值类型。现在我们可以知道参数类型与返回值类型是相同的了。 这允许我们跟踪函数里使用的类型的信息。
我们把这个版本的identity函数叫做泛型,因为它可以适用于多个类型。 不同于使用 any,它不会丢失信息,像第一个例子那像保持准确性,传入数值类型并返回数值类型。注意: 使用了<>括起来而不是()。
我们定义了泛型函数后,可以用两种方法使用。 第一种是,传入所有的参数,包含类型参数:

  1. let output = identity<string>("myString"); // type of output will be 'string'
  2. // 第二种方法更普遍。利用了类型推论 -- 即编译器会根据传入的参数自动地帮助我们确定T的类型:
  3. let output = identity("myString"); // type of output will be 'string'

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

使用泛型变量

编译器要求你在函数体必须正确的使用这个通用的类型。

  1. // 定义的泛型没有length属性,直接调用.length会报错
  2. function loggingIdentity<T>(arg: T): T {
  3. console.log(arg.length); // Error: T doesn't have .length
  4. return arg;
  5. }
  6. // 定义成数组可以调用
  7. function loggingIdentity<T>(arg: Array<T>): Array<T> {
  8. console.log(arg.length); // Array has a .length, so no more error
  9. return arg;
  10. }

泛型类型

泛型函数在声明的时候,类型和普通函数类型一样,只是签名多了一个<>:
也可以采用字面量形式定义:

  1. function identity<T>(arg: T): T {
  2. return arg;
  3. }
  4. let myIdentity: <T>(arg: T) => T = identity;
  5. // 或者用字面量形式定义
  6. let 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. let myIdentity: GenericIdentityFn = identity;

我们可能想把泛型参数当作整个接口的一个参数。 这样我们就能清楚的知道使用的具体是哪个泛型类型:

  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;

我们也可以使用不同的泛型参数名,只要在数量上和使用方式上能对应上就可以。

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

泛型类

泛型类看上去与泛型接口差不多。 泛型类使用( <>)括起泛型类型,跟在类名后面。

  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. // 或者用其他的类型限定
  9. let stringNumeric = new GenericNumber<string>();
  10. stringNumeric.zeroValue = "";
  11. stringNumeric.add = function(x, y) { return x + y; };
  12. console.log(stringNumeric.add(stringNumeric.zeroValue, "test"));

泛型约束

extents

我们定义一个接口来描述约束条件。 创建一个包含 .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. }
  8. loggingIdentity(3); // Error, number doesn't have a .length property
  9. loggingIdentity({length: 10, value: 3}); // ok

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

暂不处理.

在泛型里使用类类型

暂不处理.