Skip to content

TypeScript 泛型

简介

有些场景下,函数返回值的类型与参数类型是相关的。

ts
function getFirstElement(arr) {
  return arr[0];
}

上面示例中,函数getFirst()总是返回参数数组的第一个成员。参数数组是什么类型,返回值就是什么类型。

为反映出参数与返回值之间的类型关系,TypeScript 就引入了“泛型”(generics

ts
function getFirst<T>(arr: T[]): T {
  return arr[0];
}

函数调用时,需要提供类型参数

ts
getFirst<number>([1, 2, 3]);

上面示例中,调用函数getFirst()时,需要在函数名后面使用尖括号,给出类型参数T的值,本例是<number>

不过为了方便,函数调用时,往往省略不写类型参数的值,让 TypeScript 自己推断。有些复杂的使用场景,就必须显式给定类型参数。

类型参数的名字,可以随便取,但是必须为合法的标识符。习惯上,类型参数的第一个字符往往采用大写字母。一般会使用T(作为类型参数的名字。如果有多个类型参数,则使用 T 后面的 U、V 等字母命名,各个参数之间使用逗号(“,”)分隔。

下面是多个类型参数的例子。

ts
function map<T, U>(arr: T[], f: (arg: T) => U): U[] {
  return arr.map(f);
}

// 用法实例
map<string, number>(["1", "2", "3"], (n) => parseInt(n)); // 返回 [1, 2, 3]

总之,泛型可以理解成一段类型逻辑,需要类型参数来表达。有了类型参数以后,可以在输入类型与输出类型之间,建立一一对应关系。

泛型的基本写法

泛型主要用在四个场合:函数、接口、类和别名。

函数中的泛型

ts
function id<T>(arg: T): T {
  return arg;
}

let myId: <T>(arg: T) => T = id;

let myId: { <T>(arg: T): T } = id;

接口中的泛型

interface 也可以采用泛型的写法。

ts
interface Box<Type> {
  contents: Type;
}

let box: Box<string>;

类中的泛型写法

泛型类的类型参数写在类名后面。

ts
class Pair<K, V> {
  key: K;
  value: V;
}

泛型也可以用在类表达式

ts
const Container = class<T> {
  constructor(private readonly data: T) {}
};

const a = new Container<boolean>(true);
const b = new Container<number>(0);

下面是另一个例子。

ts
class C<NumType> {
  value!: NumType;
  add!: (x: NumType, y: NumType) => NumType;
}

let foo = new C<number>();

foo.value = 0;
foo.add = function (x, y) {
  return x + y;
};

上面示例中,先新建类C的实例foo,然后再定义value属性和add()方法。类的定义中

JavaScript 的类本质上是一个构造函数,因此也可以把泛型类写成构造函数。

ts
type MyClass<T> = new (...args: any[]) => T;

// 或者
interface MyClass<T> {
  new (...args: any[]): T;
}

// 用法实例
function createInstance<T>(AnyClass: MyClass<T>, ...args: any[]): T {
  return new AnyClass(...args);
}

上面这种 createInstance 的写法支持了:

  • 运行时动态创建实例,编译时类型安全
  • 与装饰器结合实现依赖注入。类的依赖通过外部容器注入,而不是自己创建。

type中的泛型写法

type 命令定义的类型别名,也可以使用泛型。

ts
type Nullable<T> = T | undefined | null;

定义树形结构

ts
type Tree<T> = {
  value: T;
  left: Tree<T> | null;
  right: Tree<T> | null;
};

类型参数的默认值

类型参数可以设置默认值。

ts
function getFirst<T = string>(arr: T[]): T {
  return arr[0];
}

但是,因为 TypeScript 会从实际参数推断T的值,从而覆盖掉默认值,所以下面的代码不会报错。

ts
getFirst([1, 2, 3]); // 正确

可选参数必须在必选参数之后。

ts
<T = boolean, U> // 错误

<T, U = boolean> // 正确

数组的泛型表示

数组类型有一种表示方法是Array<T> 就是泛型写法,Array是 TypeScript 原生的一个类型接口,T是它的类型参数。声明数组时,需要提供T的值。

number[]string[],只是Array<number>Array<string>的简写形式。

在 TypeScript 内部,Array是一个泛型接口,类型定义基本是下面的样子。

ts
interface Array<Type> {
  length: number;

  pop(): Type | undefined;

  push(...items: Type[]): number;

  // ...
}

类型参数的约束条件(extends)

很多类型参数并不是无限制的,对于传入的类型存在约束条件。

ts
function comp<Type>(a: Type, b: Type) {
  if (a.length >= b.length) {
    return a;
  }
  return b;
}

上面示例中,类型参数 Type 有一个隐藏的约束条件:它必须存在length属性。如果不满足这个条件,就会报错。

TypeScript 提供了一种语法,允许在类型参数上面写明约束条件,如果不满足条件,编译时就会报错。这样也可以有良好的语义,对类型参数进行说明。

类型参数的约束条件采用下面的形式。TypeParameter表示类型参数,extends是关键字,这是必须的,ConstraintType表示类型参数要满足的条件,即类型参数应该是ConstraintType的子类型。

<TypeParameter extends ConstraintType>
ts
function comp<T extends { length: number }>(a: T, b: T) {
  if (a.length >= b.length) {
    return a;
  }
  return b;
}

上面示例中,T extends { length: number }就是约束条件,表示类型参数 T 必须满足{ length: number },否则就会报错。

类型参数可以同时设置约束条件和默认值,前提是默认值必须满足约束条件。

ts
type Fn<A extends string, B extends string = "world"> = [A, B];

type Result = Fn<"hello">; // ["hello", "world"]

上面示例中,类型参数AB都有约束条件,并且B还有默认值。所以,调用Fn的时候,可以只给出A的值,不给出B的值。

另外,上例也可以看出,泛型本质上是一个类型函数,通过输入参数,获得结果,两者是一一对应关系。

如果有多个类型参数,一个类型参数的约束条件,可以引用其他参数

ts
<T, U extends T>
// 或者
<T extends U, U>

上面示例中,U的约束条件引用T,或者T的约束条件引用U,都是正确的。当然,约束条件不能引用类型参数自身。

使用注意点

使用注意点就是:少用,只在必要的地方用。

ts
function filter<T, Fn extends (arg: T) => boolean>(arr: T[], func: Fn): T[] {
  return arr.filter(func);
}

上面示例有两个类型参数,但是第二个类型参数Fn是不必要的,完全可以直接写在函数参数的类型声明里面。

ts
function filter<T>(arr: T[], func: (arg: T) => boolean): T[] {
  return arr.filter(func);
}

上面示例中,类型参数简化成了一个,效果与前一个示例是一样的。

类型参数可以是另一个泛型。

ts
type OrNull<Type> = Type | null;

type OneOrMany<Type> = Type | Type[];

type OneOrManyOrNull<Type> = OrNull<OneOrMany<Type>>;

上面示例中,最后一行的泛型OrNull的类型参数,就是另一个泛型OneOrMany