泛型
1617字约5分钟
TypeScript
2023-01-30
泛型(Generics)是一种在编译时进行类型检查的机制,它允许你在定义函数、接口或类的时候不预先指定具体的类型,而是在使用的时候指定。这样可以让你的代码更加灵活和可重用。
function getFirst<T>(arr: T[]): T {
return arr[0];
}
✨泛型的写法
泛型主要用在四个场合:函数、接口、类和别名。
✨函数
function
关键字定义的泛型函数,类型参数放在尖括号中,写在函数名后面:
function id<T>(arg: T): T {
return arg;
}
// 那么对于变量形式定义的函数,泛型有下面两种写法:
// 写法一
let myId: <T>(arg: T) => T = id;
// 写法二
let myId: { <T>(arg: T): T } = id;
✨接口
interface Box<T> {
contents: T;
}
let box: Box<string>;
// 另一个例子
interface Comparator<T> {
compareTo(value: T): number;
}
class Rectangle implements Comparator<Rectangle> {
compareTo(value: Rectangle): number {
// ...
}
}
泛型接口还有第二种写法。
interface Fn {
<T>(arg: T): T;
}
function id<T>(arg: T): T {
return arg;
}
let myId: Fn = id;
上面示例中,Fn
的类型参数 T
的具体类型,需要函数 id
在使用时提供。所以,最后一行的赋值语句不需要给出 T
的具体类型。
此外,第二种写法还有一个差异之处。那就是它的类型参数定义在某个方法之中,其他属性和方法不能使用该类型参数。前面的第一种写法,类型参数定义在整个接口,接口内部的所有属性和方法都可以使用该类型参数。
✨类
泛型类的类型参数写在类名后面。
class Pair<K, V> {
key: K;
value: V;
}
下面是继承泛型类的例子。
class A<T> {
value: T;
}
class B extends A<any> {}
上面示例中,类 A
有一个类型参数 T
,使用时必须给出 T
的类型,所以类B继承时要写成 A<any>
。
泛型也可以用在类表达式。
const Container = class<T> {
constructor(private readonly data: T) {}
};
const a = new Container<boolean>(true);
const b = new Container<number>(0);
下面是另一个例子:
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()
方法。类的定义中,属性和方法后面的感叹号是非空断言,告诉 TypeScript 它们都是非空的,后面会赋值。
JavaScript 的类本质上是一个构造函数,因此也可以把泛型类写成构造函数。
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);
}
注意
注意,泛型类描述的是类的实例,不包括静态属性和静态方法,因为这两者定义在类的本身。因此,它们不能引用类型参数。
class C<T> {
static data: T; // 报错
constructor(public value: T) {}
}
✨类型别名
type 命令定义的类型别名,也可以使用泛型。
// 例1
type Nullable<T> = T | undefined | null;
// 例2
type Container<T> = { value: T };
const a: Container<number> = { value: 0 };
const b: Container<string> = { value: "b" };
✨类型参数的默认值
类型参数可以设置默认值。使用时,如果没有给出类型参数的值,就会使用默认值。
但是 TypeScript 会从实际参数推断出T的值,从而覆盖掉默认值
function getFirst<T = string>(arr: T[]): T {
return arr[0];
}
// TypeScript 从传入参数中自动推断出 T 的值为 number
getFirst([1, 2, 3]);
类型参数的默认值,往往用在类中。
class Generic<T = string> {
list: T[] = [];
add(t: T) {
this.list.push(t);
}
}
// Generic 没传入 T 的类型,默认为 string
const g = new Generic();
// 所以方法 add 传入的参数类型是 string
g.add(4); // 报错
g.add("hello"); // 正确
一旦类型参数有默认值,就表示它是可选参数。如果有多个类型参数,可选参数必须在必选参数之后。
<T = boolean, U> // 错误
<T, U = boolean> // 正确
✨数组的泛型表示
let arr: Array<number> = [1, 2, 3];
在 TypeScript 内部,Array是一个泛型接口,类型定义基本是下面的样子:
interface Array<Type> {
length: number;
pop(): Type | undefined;
push(...items: Type[]): number;
// ...
}
TypeScript 默认还提供一个ReadonlyArray<T>
接口,表示只读数组:
function doStuff(values: ReadonlyArray<string>) {
values.push("hello!"); // 报错
}
✨类型参数的约束条件
泛型类型参数的约束条件,可以用 extends
关键字来指定。
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 },否则就会报错。
comp([1, 2], [1, 2, 3]); // 正确
comp("ab", "abc"); // 正确
comp(1, 2); // 报错
类型参数可以同时设置约束条件和默认值,前提是默认值必须满足约束条件:
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>
但是,约束条件不能引用类型参数自身:
<T extends T> // 报错
<T extends U, U extends T> // 报错
✨使用注意点
- 尽量少用泛型。
泛型虽然灵活,但是会加大代码的复杂性,使其变得难读难写。一般来说,只要使用了泛型,类型声明通常都不太易读,容易写得很复杂。因此,可以不用泛型就不要用。
- 类型参数越少越好。
多一个类型参数,多一道替换步骤,加大复杂性。因此,类型参数越少越好。
function filter<T, Fn extends (arg: T) => boolean>(arr: T[], func: Fn): T[] {
return arr.filter(func);
}
// 上面示例有两个类型参数,但是第二个类型参数Fn是不必要的,完全可以直接写在函数参数的类型声明里面。
function filter<T>(arr: T[], func: (arg: T) => boolean): T[] {
return arr.filter(func);
}
- 类型参数需要出现两次
如果类型参数在定义后只出现一次,那么很可能是不必要的。
- 泛型可以嵌套
类型参数可以是另一个泛型。
type OrNull<Type> = Type | null;
type OneOrMany<Type> = Type | Type[];
type OneOrManyOrNull<Type> = OrNull<OneOrMany<Type>>;
上面示例中,最后一行的泛型OrNull
的类型参数,就是另一个泛型OneOrMany
。