泛型
简介
在 C# 和 Java 等语言中,泛型是一种强类型编程的方式,它可以让我们在编写代码时不指定具体的类型,而是在使用时再指定类型。这样可以提高代码的复用性,减少代码的重复。TypeScript 也支持泛型。
在介绍泛型的用法之前,我们先看一个例子。下面是一个函数,它接受一个参数并返回它:
function identity(arg: any): any {
return arg
}
这个函数的特点在于接受的参数和返回的参数是一致的。但是上例我们看不出参数与返回值之间的准确的对应关系。
换种思路,类型能否像参数一样传递进来呢?这样我们就可以像变量一样,捕获并使用这个类型了。
把类型当作变量,这就是泛型的思想。
泛型用 <T>
表示,T
是一个类型变量,它可以代表任意类型。一般采用大写字母,最常用的是 T
(Type 的首字母)。
利用泛型改造上面的例子:
function identity<T>(arg: T): T {
return arg
}
let output = identity('myString')
console.log(typeof output) // string
泛型的写法
函数的泛型写法
泛型写在函数名后面的尖括号里,用 T
表示类型变量:
function identity<T>(arg: T): T {
return arg
}
对于变量形式定义的函数,有两种写法:
// 写法一
let A: <T>(arg: T) => T
// 写法二
let B: { <T>(arg: T): T }
接口的泛型写法
接口也可以使用泛型,请看最常见的写法:
interface Box<T> {
contents: T
}
和函数大同小异。
第二种写法:
interface GenericIdentityFn {
<T>(arg: T): T
}
function identity<T>(arg: T): T {
return arg
}
let myIdentity: GenericIdentityFn = identity
第一种写法,类型参数定义在整个接口,接口内部的所有属性和方法都可以使用该类型参数。而第二种写法,类型参数定义在一个函数上,只有这个函数可以使用该类型参数。
类的泛型写法
类的泛型写法和接口类似:
class Box<T> {
contents: T
}
let box = new Box<string>()
注意,泛型类描述的是类的实例,不包括静态属性和静态方法,因为这两者定义在类的本身。因此,它们不能引用类型参数。
class Box<T> {
static defaultValue: T // Static members cannot reference class type parameters.
}
类型别名的泛型写法
类型别名也可以使用泛型:
type Box<T> = { contents: T }
数组的泛型写法
前文提到过,数组有一种泛型写法,即:
let list: Array<number> = [1, 2, 3]
// 等价于 =>
let list: number[] = [1, 2, 3]
泛型的默认值
泛型可以有默认值,这样在使用时可以不指定类型参数:
function identity<T = string>(arg: T): T {
return arg
}
let output = identity(42) // 类型推断为 number,覆盖掉默认值
泛型的约束
有时候,类型参数并不适用所有值,此时需要为其添加某些约束条件。
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
函数可以正常工作,此时添加上类型约束是必要的。
泛型约束写法如下:
<TypeParameter extends ConstraintType>
TypeParameter
表示类型参数,extends
是关键字,这是必须的,ConstraintType
表示类型参数要满足的条件,即类型参数应该是 ConstraintType 的子类型。
类型参数可以同时设置约束条件和默认值,前提是默认值必须满足约束条件。
type Fn<A extends string, B extends string = 'world'>
= [A, B];
type Result = Fn<'hello'> // ["hello", "world"]
如果有多个类型参数,一个类型参数的约束条件,可以引用其他参数。
<T, U extends T>
// 或者
<T extends U, U>
如上,U的约束条件引用T,或者T的约束条件引用U,都是正确的。
但是,约束条件不能引用类型参数自身。
<T extends T> // 报错,类型参数不能引用自身
<T extends U, U extends T> // 报错,多个类型参数也不能互相约束
T的约束条件不能是T自身。同理,多个类型参数也不能互相约束(即T的约束条件是U、U的约束条件是T),因为互相约束就意味着约束条件就是类型参数自身。