TypeScript 泛型
1. 简介
有时候函数返回值的类型与参数类型是相关的,为了表示这种类型关联关系,TS 提供了“泛型”的概念,泛型的特点就是带有“类型参数”(type parameter):
function getFirst<T>(arr: T[]): T {
return arr[0];
}
2
3
上面的示例中,函数名后尖括号内的部分就是类型参数,称为函数的泛型。本例只有一个类型参数,实际使用时根据需要可以定义多个,类型参数的名字,可以随便取,但是必须为合法的标识符。习惯上,类型参数的第一个字符往往采用大写字母。一般会使用 T(type 的第一个字母)作为类型参数的名字。如果有多个类型参数,则使用 T 后面的 U、V 等字母命名,各个参数之间使用逗号(“,”)分隔。定义泛型后,后续用到同名类型的地方就表示的同一个类型。
泛型函数在调用时,需要提供类型参数,不过为了方便也可以让 TS 自己推断类型参数:
getFirst<number>([1, 2, 3]);
// 或者:getFirst([1, 2, 3])
2
有些复杂的使用场景,TypeScript 可能推断不出类型参数的值,这时就必须显式给出了:
function comb<T>(arr1: T[], arr2: T[]): T[] {
return arr1.concat(arr2);
}
comb([1, 2], ["a", "b"]); // 报错
comb<number | string>([1, 2], ["a", "b"]); // 正确
2
3
4
5
6
2. 泛型的写法
泛型主要用在四个场合:函数、接口、类和别名。
- 函数的泛型写法:上面的例子中已经提及了函数的泛型写法
- 接口的泛型写法:
interface 也可以采用泛型,且有两种写法:
// 写法一
interface Box<Type> {
contents: Type;
}
let box: Box<string>;
// 写法二
interface Fn {
<Type>(arg: Type): Type;
}
function id<Type>(arg: Type): Type {
return arg;
}
let myId: Fn = id;
2
3
4
5
6
7
8
9
10
11
12
13
14
第二种写法有一个差异之处。那就是它的类型参数定义在某个方法之中,其他属性和方法不能使用该类型参数。第一种写法,类型参数定义在整个接口,接口内部的所有属性和方法都可以使用该类型参数
- 类的泛型写法
类的泛型写法与函数类型,泛型参数写在类名后面。当继承一个泛型类时,必须给出泛型的类型,所以继承后新的类会丢失基类中的泛型,这时需要在新的类中也添加泛型,并与基类泛型的类型兼容:
class A<T> {
value!: T;
}
class B extends A<any> {}
// 或者
class B<T> extends A<T> {}
2
3
4
5
6
7
JS 的类本质上是一个构造函数,因此也可以把泛型类写成构造函数。
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);
}
2
3
4
5
6
7
8
9
10
11
WARNING
注意,泛型类描述的是类的实例,不包括静态属性和静态方法,因为这两者定义在类的本身。因此,它们不能引用类型参数。
class C<T> {
static data: T; // 报错
constructor(public value: T) {}
}
2
3
4
- type 的泛型写法
type
命令定义的类型别名,也可以使用泛型:
type Nullable<T> = T | undefined | null;
上面示例中,Nullable<T>
是一个泛型,只要传入一个类型,就可以得到这个类型与 undefined 和 null 的一个联合类型。
3. 类型参数的默认值
类型参数可以设置默认值。使用时,如果没有给出类型参数的值,就会使用默认值:
function getFirst<T = string>(arr: T[]): T {
return arr[0];
}
2
3
但是,因为 TS 会从实际参数推断出 T 的值,从而覆盖掉默认值,所以 getFirst([1, 2, 3])
不会报错。
如果有多个类型参数默认值,可选类型参数必须在必须参数之后。
4. 类型参数的约束条件
很多类型参数并不是无限制的,对于传入的类型存在约束条件。TS 提供 <TypeParameter extends ConstraintType>
语法,允许在类型参数上面写明约束条件,如果不满足条件,编译时就会报错。这样也可以有良好的语义,对类型参数进行说明:
function comp<T extends { length: number }>(a: T, b: T) {
if (a.length >= b.length) {
return a;
}
return b;
}
comp("ab", "abc"); // 正确
comp(1, 2); // 报错
2
3
4
5
6
7
8
9
上面示例中,T extends { length: number }
就是约束条件,表示类型参数 T
必须满足 { length: number }
,否则就会报错。
类型参数可以同时设置约束条件和默认值,前提是默认值必须满足约束条件:
type Fn<A extends string, B extends string = "world"> = [A, B];
type Result = Fn<"hello">; // ["hello", "world"]
2
3
如果有多个类型参数,一个类型参数的约束条件,可以引用其他参数:
<T, U extends T>
// 或者
<T extends U, U>
2
3
5. 使用注意点
泛型使用时需要注意:
- 尽量少用泛型。
泛型虽然灵活,但是会加大代码的复杂性,使其变得难读难写。一般来说,只要使用了泛型,类型声明通常都不太易读,容易写得很复杂。因此,可以不用泛型就不要用。
- 类型参数越少越好。
多一个类型参数,多一道替换步骤,加大复杂性。因此,类型参数越少越好。
- 类型参数需要出现两次。
如果类型参数在定义后只出现一次,那么很可能是不必要的。
- 泛型可以嵌套。
类型参数可以是另一个泛型。