跳到主要内容

类型细化

有的时候我们会对某个值做类型判断,而 TS 能从中推断该值更具体的类型:

function padLeft(padding: number | string, input: string) {
if (typeof padding === 'number') {
// 这里 TS 会推断出 `padding` 是 `number` 类型
return ' '.repeat(padding) + input;
}
// 这里 TS 会推断出 `padding` 是 `string` 类型
return padding + input;
}

这种类型推断的特性称为“类型细化”( Narrowing ),一般存在于各种控制语句上,比如 if-else、三元条件语句、循环、真值检查等。有许多种情况可以应用这一特性。

typeof 类型保护

typeof 是 JS 里的操作符,能够在运行时里提供值类型信息。TS 能够分析的 typeof 的返回值有:

  • "string"
  • "number"
  • "bigint"
  • "boolean"
  • "undefined"
  • "object"
  • "function"

TS 有着自己的 typeof 处理,因为 JavaScript 中可能会存在一些奇怪的行为,比如 null 会被认为是 object 类型(实际上 null 自身是一种类型):

function printAll(strs: string | string[] | null) {
if (typeof strs === 'object') {
// ERROR: 这里的 `strs` 也有可能是 `null`
for (const s of strs) {
console.log(s);
}
} else if (typeof strs === 'string') {
// ...
}
}

像 typeof 这样类型检查的语句在 TS 中称为类型保护( Type guard )。

真值检查

在 JS 里我们可以在条件语句、&&||if 语句或否定符 ! 等里使用任何表达式。比如我们可以在 if 里放非 boolean 类型的条件语句。JS 会将条件语句强制转为布尔值,比如这些值会被转为 false

  • 0

  • NaN

  • "" ,即空字符串

  • 0n ,即 bigint 中的 0

  • null

  • undefined

你也可以用 Boolean 函数将值转为布尔值,或者用双否定 !! 。而后者其实会推断为一个布尔值字面量类型(比如例子中为 true ),前者仅仅是个 boolean 类型:

Boolean('hello'); // 类型为 `boolean` ,值为 `true`
!!'world'; // 类型和值均为 `true`

我们可以利用这一行为来进行空值检查:

function printAll(strs: string | string[] | null) {
if (strs && typeof strs === 'object') {
for (const s of strs) {
console.log(s);
}
} else if (typeof strs === 'string') {
// ...
}
}

但是 不要 写成如下的例子,因为空字符串是 string 类型,但是会转为布尔值 false

function printAll(strs: string | string[] | null) {
if (strs) {
if (typeof strs === 'object') {
for (const s of strs) {
console.log(s);
}
} else if (typeof strs === 'string') {
// ...
}
}
}

判等语句

TS 会对 switch 语句,以及 ===!====!= 等判等操作中来细化类型:

function example(x: string | number, y: string | boolean) {
if (x === y) {
// 这里 `x` 和 `y` 类型均被细化为 `string`
x.toUpperCase();
y.toLowerCase();
} else {
// 这里 `x` 类型被细化为 `string | number`
console.log(x);
}
}

对于 ==!= 这类弱化的判等,TS 也会有正确的类型细化:

function anotherExample(x: number | null | undefined) {
if (x != null) {
// 这里 `x` 类型被细化为 `number`
console.log(x);
}
}

in 操作符

TS 会根据 in 操作语句来细化类型:

type Fish = { swim: () => void };
type Bird = { fly: () => void };

function move(animal: Fish | Bird) {
if ('swim' in animal) {
return animal.swim();
}
return animal.fly();
}

instanceof 操作符

JS 中 instanceof 用于判断是否在某个类的原型链上。TS 也会据此细化类型:

function logValue(x: Date | string) {
if (x instanceof Date) {
// 这里 `x` 类型被细化为 `Date`
console.log(x.toUTCString());
} else {
console.log(x.toUpperCase());
}
}

赋值语句

在赋值的时候,TS 也会细化类型,不需要我们自己判断:

// 只有声明语句能确定变量类型
let x = Math.random() < 0.5 ? 10 : 'hello world!';
x = 1; // `x` 类型被细化为 `number`
x = 'goodbye!'; // `x` 类型被细化为 `string`
x = false; // ERROR, 不能将 `boolean` 赋值给 `string | number` 类型

类型细化不是类型改变。

类型断言

函数的返回值可以是个类型断言,这将告诉 TS 是否需要类型细化:

function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}

let pet = getSmallPet();

// TS 会根据类型断言对 pet 进行类型细化
if (isFish(pet)) {
pet.swim();
} else {
pet.fly();
}

如果没有 pet is Fish 的类型断言,那么条件语句里的 pet 将不会做类型细化。

可区分的联合类型

在下面这个例子中,Shape 是一个联合类型:

interface Circle {
kind: 'circle';
radius: number;
}

interface Square {
kind: 'square';
sideLength: number;
}

type Shape = Circle | Square;

但在处理 Shape 对象时,TS 可以根据 kind 属性来细化类型:

function getArea(shape: Shape) {
switch (shape.kind) {
case 'circle':
// 这里 `shape` 类型被细化为了 `Circle` ,下同
return Math.PI * shape.radius ** 2;
case 'square':
return shape.sideLength ** 2;
}
}

never 类型

在细化过程中,如果遇到了不可能存在的情况,TS 会用 never 类型代替:

function print(x: number | string) {
if (typeof x === 'number') {
// ...
} else if (typeof x === 'string') {
// ...
} else {
// 这是一个不可能达到的代码块,所以 TS 会将 x 类型标为 `never`
}
}

参考

Narrowing - TypeScript