Skip to content

泛型

简介

在 C# 和 Java 等语言中,泛型是一种强类型编程的方式,它可以让我们在编写代码时不指定具体的类型,而是在使用时再指定类型。这样可以提高代码的复用性,减少代码的重复。TypeScript 也支持泛型。

在介绍泛型的用法之前,我们先看一个例子。下面是一个函数,它接受一个参数并返回它:

typescript
function identity(arg: any): any {
  return arg
}

这个函数的特点在于接受的参数和返回的参数是一致的。但是上例我们看不出参数与返回值之间的准确的对应关系。

换种思路,类型能否像参数一样传递进来呢?这样我们就可以像变量一样,捕获并使用这个类型了。

把类型当作变量,这就是泛型的思想。

泛型用 <T> 表示,T 是一个类型变量,它可以代表任意类型。一般采用大写字母,最常用的是 T(Type 的首字母)。

利用泛型改造上面的例子:

typescript
function identity<T>(arg: T): T {
  return arg
}

let output = identity('myString')

console.log(typeof output) // string

泛型的写法

函数的泛型写法

泛型写在函数名后面的尖括号里,用 T 表示类型变量:

typescript
function identity<T>(arg: T): T {
  return arg
}

对于变量形式定义的函数,有两种写法:

typescript
// 写法一
let A: <T>(arg: T) => T

// 写法二
let B: { <T>(arg: T): T }

接口的泛型写法

接口也可以使用泛型,请看最常见的写法:

typescript
interface Box<T> {
  contents: T
}

和函数大同小异。

第二种写法:

typescript
interface GenericIdentityFn {
  <T>(arg: T): T
}

function identity<T>(arg: T): T {
  return arg
}

let myIdentity: GenericIdentityFn = identity

第一种写法,类型参数定义在整个接口,接口内部的所有属性和方法都可以使用该类型参数。而第二种写法,类型参数定义在一个函数上,只有这个函数可以使用该类型参数。

类的泛型写法

类的泛型写法和接口类似:

typescript
class Box<T> {
  contents: T
}

let box = new Box<string>()

注意,泛型类描述的是类的实例,不包括静态属性和静态方法,因为这两者定义在类的本身。因此,它们不能引用类型参数。

typescript
class Box<T> {
  static defaultValue: T // Static members cannot reference class type parameters.
}

类型别名的泛型写法

类型别名也可以使用泛型:

typescript
type Box<T> = { contents: T }

数组的泛型写法

前文提到过,数组有一种泛型写法,即:

typescript
let list: Array<number> = [1, 2, 3]
// 等价于 =>
let list: number[] = [1, 2, 3]

泛型的默认值

泛型可以有默认值,这样在使用时可以不指定类型参数:

typescript
function identity<T = string>(arg: T): T {
  return arg
}

let output = identity(42) // 类型推断为 number,覆盖掉默认值

泛型的约束

有时候,类型参数并不适用所有值,此时需要为其添加某些约束条件。

typescript
interface Lengthwise {
  length: number
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
  console.log(arg.length)
  return arg
}

loggingIdentity('hello') // 5
loggingIdentity([1, 2, 3]) // 3
loggingIdentity(42) // Argument of type '42' is not assignable to parameter of type 'Lengthwise'.

上例中,T 必须包含一个 length 属性,这样才能保证 loggingIdentity 函数可以正常工作,此时添加上类型约束是必要的。

泛型约束写法如下:

typescript
<TypeParameter extends ConstraintType>

TypeParameter表示类型参数,extends是关键字,这是必须的,ConstraintType表示类型参数要满足的条件,即类型参数应该是 ConstraintType 的子类型。

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

typescript
type Fn<A extends string, B extends string = 'world'>
  =  [A, B];

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

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

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

如上,U的约束条件引用T,或者T的约束条件引用U,都是正确的。

但是,约束条件不能引用类型参数自身。

typescript
<T extends T>               // 报错,类型参数不能引用自身
<T extends U, U extends T>  // 报错,多个类型参数也不能互相约束

T的约束条件不能是T自身。同理,多个类型参数也不能互相约束(即T的约束条件是U、U的约束条件是T),因为互相约束就意味着约束条件就是类型参数自身。