TypeScript 类型缩小
当一个值的类型为联合类型时,具体代码中我们可能知道这个值为某一种具体的类型,但 TS 编译器不知道。这就会导致针对具体类型的操作在这段代码中报类型错误。例如:
function padLeft(padding: number | string, input: string): string {
// 报错,padding 有可能是字符串
return " ".repeat(padding) + input;
}
2
3
4
上面的报错告诉我们,需要先明确检查 padding
的类型是数字类型后才能进行操作。所以我们可以这样修改:
function padLeft(padding: number | string, input: string): string {
if (typeof padding === "number") {
return " ".repeat(padding) + input;
}
return padding + input;
}
2
3
4
5
6
上面将一个联合类型先通过判断确定为一个准确的类型,再进行操作就称为“类型缩小”(Norrowing),或者“类型收窄”。TS 类型缩小有几种方式:
1. typeof
JS 的 typeof 操作符可以得到一组特定的类型字符串:
string
number
bigint
boolean
symbol
undefined
object
function
TS 可以将 typeof 操作理解为缩小类型到操作结果的类型。
typeof 也有一些问题,例如经典的 typeof null === 'object'
,TS 在检查 typeof value === 'object'
时会考虑这种情况,所以收窄后的类型可能包括 null
:
function printAll(strs: string | string[] | null) {
if (typeof strs === "object") {
// 报错:strs 可能是 null
for (const s of strs) {
// ...
}
} else {
// ...
}
}
2
3
4
5
6
7
8
9
10
2. in
JS 的 in
操作符用于确定对象或其原型链是否具有某个属性,TS 利用这种特性也可以进行类型缩小:
type Fish = { swim: () => void };
type Bird = { fly: () => void };
function move(animal: Fish | Bird) {
if ("swim" in animal) {
return animal.swim();
}
return animal.fly();
}
2
3
4
5
6
7
8
9
10
上面示例中,通过判断是否存在 swim
方法,可以确定类型是 Fish
。如果某个类具有此方法,但是是可选的,那在使用 in
操作符缩小类型时,这个类型会出现在“真”和“假”两种情况中:
type Fish = { swim: () => void };
type Bird = { fly: () => void };
type Human = { swim?: () => void; fly?: () => void };
function move(animal: Fish | Bird | Human) {
if ("swim" in animal) {
// 类型为: Fish | Human
animal;
} else {
// 类型为: Bird | Human
animal;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
3. instanceof
JS 的 instanceof
操作符可以判断对象是否是某个类的实例(检查类的原型链中是否包含对象的原型对象),TS 也可以利用 instanceof
来缩小类型:
function logValue(x: Date | string) {
if (x instanceof Date) {
// 类型为: Date
console.log(x.toUTCString());
} else {
// 类型为: string
console.log(x.toUpperCase());
}
}
2
3
4
5
6
7
8
9
4. 赋值缩小
在为变量赋值时,TS 会检查所赋值的类型,并适当的缩小变量原本的类型:
// string |number 类型
let x = Math.random() < 0.5 ? 10 : "hello world!";
// number 类型
x = 1;
// string 类型
x = "goodbye!";
2
3
4
5
6
7
8
即使在第一次修改值后类型变为了 number
,但是仍然会以值的声明类型为准,所以可以再赋值为字符串。
5. 真假判断缩小
JS 中,可以使用 &&
、||
、if
、!
等运算符来判断一个表达式是否为“真”,这些运算符会将值强制转换为布尔值来进行判断。利用这种行为,可以过滤掉 null
、undefined
这样的值:
function printAll(strs: string | string[] | null) {
if (strs && typeof strs === "object") {
// 不再报错,null的情况已被过滤
for (const s of strs) {
// ...
}
} else {
// ...
}
}
2
3
4
5
6
7
8
9
10
6. 相等性检查
TS 还可以用 switch
、===
、!==
、==
和 !=
来缩小类型:
function example(x: string | number, y: string | boolean) {
if (x === y) {
// x 和 y 的公共类型是 string,所以这里能确定 x、y 都为字符串
x.toUpperCase();
y.toLowerCase();
} else {
// 类型为: string | number
console.log(x);
// 类型为: string | boolean
console.log(y);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
上面例子中通过相等判断,TS 知道了 x
和 y
的类型也是相等的,于是可以确定为公共类型 string
。
使用 ==
和 !=
可以惊醒更宽松的相等性检查。我们知道 undefined == null
成立,所以 TS 在检查时,除了确定为判断的情况,还会包含相等类型的情况:
interface Container {
value: number | null | undefined;
}
function multiplyValue(container: Container, factor: number) {
// 这里的 != null 判断,也包含了对 undefined 判断
if (container.value != null) {
container.value *= factor;
}
}
2
3
4
5
6
7
8
9
10
7. 类型断言缩小
前文中介绍了几种类型断言的方式,TS 可以利用类型断言来得到准确的类型,也达到了类型缩小的目的(建议只使用断言函数或类型保护函数来缩小类型):
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
if (isFish(pet)) {
pet.swim();
} else {
pet.fly();
}
2
3
4
5
6
7
8
9
8. 可辨识联合
目前为止,我们看到的大多数示例都集中在使用简单类型(如 string
、boolean
、number
)缩小单个变量。 虽然这很常见,但大多数时候我们都要处理更复杂的结构。
例如定义了一个形状接口 Shape
,需要定义求面积的函数:
interface Shape {
kind: "circle" | "square";
radius?: number;
sideLength?: number;
}
function getArea(shape: Shape) {
if (shape.kind === "circle") {
// 报错 radius 可能为 undefined
return Math.PI * shape.radius ** 2;
}
// 报错 sideLength 可能为 undefined
return shape.sideLength ** 2;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
因为我们定义的类型并不能准确表达 kind 为某个值时其他属性的关系,所以 TS 报错是没有问题的。虽然可以用非空断言(!
)来避免报错,但并不是好的处理方式。我们可以分开定义圆和正方形的接口类型再试试:
interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
sideLength: number;
}
type Shape = Circle | Square;
function getArea(shape: Shape) {
if (shape.kind === "circle") {
return Math.PI * shape.radius ** 2;
}
return shape.sideLength ** 2;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
可以看到现在代码不再报错。因为 kind
是公共属性,且是常值类型,TS 检查后就可以确定具体的类型完成缩小。同样的检查也适用于 switch
语句,并且不需要使用非空断言:
function getArea(shape: Shape) {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.sideLength ** 2;
}
}
2
3
4
5
6
7
8
9
这里最重要的是两个类型具有特定的字段,所以 TS 能根据特定字段来分辨联合类型成员。这种带有特定字段的联合类型称为“可辨识联合”。
9. 控制流分析
上文提到的示例已经使用到了控制流分析缩小。当在一个分支中收窄类型后,通过控制流分析可以判断出另一个分支中准确的类型:
function padLeft(padding: number | string, input: string) {
if (typeof padding === "number") {
// 缩小为数字类型
return " ".repeat(padding) + input;
}
// 可以分析出这里为字符串类型
return padding + input;
}
2
3
4
5
6
7
8