第 6 章 TypeScript 类型进阶
6.1 泛型
泛型是一种编程风格或编程范式,它允许在程序中定义形式类型参数,然后在泛型实例化时使用实际类型参数来替换形式类型参数
泛型简介
简单泛型示例
function identity<T>(arg: T): T {
return arg;
}
形式类型参数
形式类型参数声明
<TypeParameter, TypeParameter, ...>形式类型参数名通常有两种风格
- 以大写字母
T开头,后接描述性名字,如TResponse - 以单个大写字母命名,由
T开始,参数少时建议采用这种风格
- 以大写字母
类型参数默认类型
<T = DefaultType>T:形式类型参数DefaultType:默认类型
默认类型也可以引用参数列表中排在前面的类型。示例如下
<T, U = T>可选的类型参数 可选类型参数须排在必须类型参数之后。示例如下
<T, U = boolean>
实际类型参数
当显式地传入实际类型参数时,只有必选类型参数是一定要提供的
示例如下
function identity<T, U = boolean>(arg: T): T {
return arg;
}
identity<number>(1);
identity<Date>(new Date());
identity<string>('1');
identity<string, string>('1');
泛型约束
泛型约束声明
在泛型的形式类型参数上允许定义一个约束条件。语法示例如下
<TypeParameter extends ConstraintType = DefaultType>
TypeParameter:形式类型参数名extends:关键字ConstraintType:类型,用于约束TypeParameter的可选类型范围DefaultType:默认类型,必须满足泛型约束
使用示例如下
interface Point {
x: number;
y: number;
}
function identity<T extends Point>(x: T): T {
return x;
}
identity({ x: 0, y: 0 });
泛型约束引用类型参数
约束类型允许引用当前形式类型参数列表中的其他类型参数,但不允许直接或间接地引用自身。示例如下
// 正确
<T, U extends T>
<T extends U, U>
// 错误
<T extends T>
<T extends U, U extends T>
基约束
本质上,每个类型参数都有一个基约束 Base Constraint,它与是否再形式类型参数上定义了泛型约束无关。类型参数的实际类型一定是其基约束的子类型。对于任意的类型参数 T,其基约束的计算规则有三个
- 如果类型参数
T声明了泛型约束,且泛型约束为另一个类型参数U,T的基约束为U - 如果类型参数
T声明了泛型约束,且泛型约束为某一具体类型Type,则T的基约束为该Type - 如果类型参数
T没有声明泛型约束,则T的基约束为空对象类型字面量{},除了undefined类型和null类型外,都可以赋值给空对象类型字面量
常见错误
示例如下
interface Point {
x: number;
y: number;
}
function f<T extends Point>(arg: T): T {
return { x: 0, y: 0 }; // 错误
}
返回值类型须与参数 arg 的类型相同,而不能仅满足泛型约束,通过下一个的例子更容易理解。示例如下
function f<T extends boolean>(obj: T): T {
return true;
}
f<false>(false); // 错误
参数 obj 的类型为 false,因此 T 的类型为 false,但函数内部返回了 true
泛型函数
若一个函数的函数签名中带有类型参数,那么它是一个泛型函数。泛型函数中的类型参数既可用于形式参数的类型,也可用于函数返回值类型
泛型函数定义
- 泛型调用签名语法如下
<T>(x: T): T - 泛型构造签名语法如下
new <T>(): T;
泛型函数类型推断
大部分情况下,编译器能够自动推断出泛型函数的实际类型参数,甚至比显式指定实际类型参数更加精确。示例如下
function f<T>(x: T): T {
return x;
}
const a = f('a'); // 推断出实际类型为 'a'
const b = f('b'); // 推断出实际类型为 'b'
泛型函数注意事项
如果一个函数既可以定义为非泛型函数,又可以定义为泛型函数,推荐使用非泛型函数
泛型接口
若接口的定义中带有类型参数,则它是泛型接口。示例如下
interface MyArray<T> extends Array<T> {
first: T | undefined;
last: T | undefined;
}
在引用泛型接口时,必须指定实际类型参数,除非类型参数定义了默认类型。示例如下
const a: Array<number> = [0, 1, 2];
泛型类型别名
若类型别名的定义中带有类型参数,则它是泛型类型别名。语法示例如下
type Nullable<T> = T | undefined | null;
使用示例如下
定义简单容器类型
type Container<T> = { value: T }; const a: Container<number> = { value: 0 }; const b: Container<string> = { value: 'b' };定义树形结构
type Tree<T> = { value: T; left: Tree<T> | null; right: Tree<T> | null; }; const tree: Tree<number> = { value: 0, left: { value: 1, left: { value: 3, left: null, right: null, }, right: null, }, right: { vlaue: 2, left: null, right: null, }, };
泛型类
若类的定义中带有类型参数,则它是泛型类。语法示例如下
// 类声明
class Container<T> {
constructor(private readonly data: T) {}
}
const a = new Container<boolean>(true);
const b = new Container<number>(0);
// 类表达式
const Container = class<T> {
constructor(private readonly data: T) {}
};
泛型类中的类型参数允许在类的继承语句和接口实现语句中使用,即 extends 语句和 implements 语句。示例如下
interface A<T> {
a: T;
}
class Base<T> {
b?: T;
}
class Derived<T> extends Base<T> implements A<T> {
constructor(public readonly a: T) {
super();
}
}
每个类声明都会创建两种类型:类的实例类型和类的构造函数类型。
泛型类描述的是类的实例类型。因为类的静态成员是类构造函数类型的一部分,所有泛型类型参数不能用于类的静态成员。示例如下
class Container<T> {
static version: T; // 错误,静态成员不允许引用类型参数
}
6.2 局部类型
TypeScript 支持声明具有块级作用域的局部类型,主要包括
- 局部枚举类型
- 局部类类型
- 局部接口类型
- 局部类型别名
示例如下
function f<T>() {
enum E {
A,
B,
}
class C {
x: string | undefined;
}
// 可以带有泛型参数
interface I<T> {
x: T;
}
// 可以引用其他局部类型
type A = E.A | E.B;
}
6.3 联合类型
联合类型由一组有序的成员类型构成,其表示一个值的类型可以为若干种类型之一,联合类型通过联合类型字面量来定义
联合类型字面量
联合类型由两个或以上的成员类型构成,各成员类型之间使用 | 分隔,成员类型可以为任意类型。示例如下
type NumericType = number | bigint | string[] | { x: number } | (() => void);
- 成员类型存在相同类型时,将被合并
- 绝大部分情况下,成员类型的顺序不影响联合类型的结果
- 对部分成员类型使用分组运算符
(),不影响联合类型的结果 - 如果某个成员类型
A是其他成员类型B的子类型时,可以省略该成员类型A
联合类型的类型成员
与接口类型一样,联合类型作为一个整体也可以有类型成员,其类型成员由其成员类型决定
属性签名
若联合类型 U 中的每个成员类型都包含一个同名的属性签名 M
- 则联合类型
U也包含属性签名M M类型为每个成员类型中M的类型组成的联合类型- 若
M在某个成员类型中为可选类型,则在U中也为可选,否则为必选
示例如下
interface Circle {
area: number;
radius: number;
}
interface Rectangle {
area: number;
radius: string;
width: number;
height: number;
}
type Shape = Circle | Rectangle;
declare const s: Shape;
s.area; // number
s.radius; // number | string
s.width; // 错误
s.height; // 错误
索引签名
此处的字符串索引签名和数值索引签名一致,统一用索引签名代替
如果联合类型中每个成员都包含索引签名,则该联合类型也拥有了索引签名,否则没有。索引签名中的索引值类型为每个成员类型中索引值类型的联合类型
示例如下
interface T0 {
[prop: string]: number;
[prop: number]: number;
}
interface T1 {
[prop: string]: bigint;
[prop: number]: bigint;
}
// T 和 T0T1 相同
type T = T0 | T1;
interface T0T1 {
[prop: string]: number | biging;
[prop: number]: number | biging;
}
调用签名与构造签名
与索引签名同理。示例如下
interface T0 {
(name: string): number;
new (name: string): Date;
}
interface T1 {
(name: string): bigint;
new (name: string): Error;
}
// T 和 T0T1 相同
type T = T0 | T1;
interface T0T1 {
(name: string): number | bigint;
new (name: string): Date | Error;
}
6.4 交叉类型
交叉类型在逻辑上与联合类型是互补的,交叉类型表示一个值同时属于多种类型
交叉类型通过交叉类型字面量来定义
交叉类型字面量
使用 & 符号分隔各成员类型。示例如下
interface Clickable {
click(): void;
}
interface Focusable {
focus(): void;
}
type T = Clickable & Focusable;
成员类型的运算
- 多个相同的成员类型会被合并
- 绝大部分情况下,成员类型的顺序不影响结果类型
- 当涉及到调用签名重载或构造签名重载时,则不能随便调用成员类型顺序,因为这会影响到重载签名的顺序
- 对部分类型成员使用分组运算符
()不影响结果类型 - 交叉类型中使用原始类型成员时,结果类型将为
never类型,虽然合法但不常见。示例如下type T = boolean & number & string;
交叉类型的类型成员
属性签名
- 只要任何一个成员类型中包含了属性签名
M,则该交叉类型也包含M - 交叉类型
M的类型为各成员类型中M的交叉类型 M在所有成员类型中都为可选时,交叉类型中的M也为可选,否则为必选
索引签名
- 只要任何一个成员类型包含了索引签名,则该交叉类型也拥有了索引签名,否则没有
- 交叉类型索引签名中的索引值类型为每个成员类型中索引值类型的交叉类型
调用签名与构造签名
若成员类型中含有调用签名或构造签名,则这些调用签名和构造签名将以成员类型的先后顺序合并到交叉类型中
因此当交叉类型中存在重载签名时,要留意顺序
交叉类型与联合类型
&符号相当于数学中的×号|符号相当于数学中的+号
因此
- 当两者同时使用时,交叉类型优先级更高
- 分配律性质,满足数学中的
乘法分配律。示例如下type T = (string | 0) & (number | 'a'); type T = (string & number) | (string & 'a') | (0 & number) | (0 & 'a'); // 没有交集的原始类型的交叉类型为 never type T = never | 'a' | 0 | 'never'; // never 是所有类型的子类型 // 若某成员类型是其他成员类型的子类型,则可删除该成员类型,因此删除 never type T = 'a' | 0;