Typescript 中的泛型

47 阅读3分钟

泛型函数

对于一个有输入和输出的函数, 如:

// 返回一个数组的第一个元素
function firstElement(arr: any[]) {
  return arr[0];
}

在这里,我们不知道 arr 的类型,所以使用 any,最终返回的也是一个 any。这样就会丢失类型检查的能力。

我们更希望返回数组元素的类型

在 Typescript 中,当我们想描述两个值之间关系时,使用泛型。

在函数签名中声明一个类型参数

function firstElement<Type>(arr: Type[]): Type | undefined {
  return arr[0];
}

通过增加类型参数 Type, 我们可以捕获用户传入的类型,然后使用这个类型,输出这个类型。

// s is of type 'string'
const s = firstElement(["a", "b", "c"]);
// n is of type 'number'
const n = firstElement([1, 2, 3]);
// u is of type undefined
const u = firstElement([]);

这里不必指定 Type 的具体类型,因为具体的 type 会在执行过程中被推理出来。

我们也可以使用多个 type 参数。例如:

function map<Input, Output>(
  arr: Input[],
  func: (arg: Input) => Output
): Output[] {
  return arr.map(func);
}

// Parameter 'n' is of type 'string'
// 'parsed' is of type 'number[]'
const parsed = map(["1", "2", "3"], (n) => parseInt(n));

约束参数类型

一般通过 extends 关键字来约束泛型的类型。

求两个参数中最长的那个

这要求 longest 的 2 个参数都必须要有 length 属性,且为 number 类型。这里的 2 个入参,可以为数组,也可以为字符串,或者其他有 length 属性的类型。所以这里的入参,我们用泛型来表示。但是,因为我们要求入参必须有 length 属性,所以我们需要对泛型进行约束。通过 extends 关键字,我们要求泛型必须有 length 属性。

function longest<Type extends { length: number }>(a: Type, b: Type) {
  if (a.length >= b.length) {
    return a;
  } else {
    return b;
  }
}

// longerArray is of type 'number[]'
const longerArray = longest([1, 2], [1, 2, 3]);
// longerString is of type 'alice' | 'bob'
const longerString = longest("alice", "bob");
// Error! Numbers don't have a 'length' property
const notOK = longest(10, 100);

泛型,就是将 2 个或多个值与相同类型相关联。

在泛型的调用中指定参数类型

function combine<Type>(arr1: Type[], arr2: Type[]): Type[] {
  return arr1.concat(arr2);
}

通常,我们在调用时,typescript 会自动推断出类型,这里与定义的泛型不一致,所以会报错。

const arr = combine([1, 2, 3], ["hello"]);

// Type 'string' is not assignable to type 'number'.

这时,我们可以指定泛型的类型

const arr = combine<number | string>([1, 2, 3], ["hello"]);

编写好的泛型

    1. 如果可能,使用类型参数本身而不是对其进行约束
    1. 始终使用尽可能少的类型参数
    1. 如果一个类型参数只出现在一个位置,强烈重新考虑你是否真的需要它(即类型参数在函数签名中至少有效的使用 2 次)
    1. 为回调编写函数类型时,切勿编写可选参数,除非您打算在不传递该参数的情况下调用该函数
    function myForEach(arr: any[], callback: (arg: any, index?: number) => void) {
      for (let i = 0; i < arr.length; i++) {
        callback(arr[i], i);
      }
    }
    
    myForEach([1, 2, 3], (a, i) => {
      console.log(i.toFixed());
    });
    // 'i' is possibly 'undefined'.
    
    1. 实现的签名从外部是不可见的。编写重载函数时,您应该始终在函数的实现之上有两个或更多签名。
    function fn(x: string): void;
    function fn() {
      // 这里是函数的实现部分,此处定义的没有参数的签名,是不可见的
      // ...
    }
    // Expected to be able to call with zero arguments
    fn();