第三章:收窄与守护:驾驭类型的不确定性

第三章:收窄与守护:驾驭类型的不确定性

摘要: 欢迎来到第三章。在上一章,我们学会了如何使用联合类型 (|) 来为变量定义多种可能性。但“定义”只是第一步,“使用”才是真正的挑战。本章,我们将直面操作联合类型时遇到的核心痛点,并系统性地学习 TypeScript 提供的强大武器库——类型守护 (Type Guards)。您将掌握如何使用 typeofin 以及自定义的 is 谓词,在代码块中智能地 收窄 类型范围,将不确定的联合类型转化为精确的、可安全操作的具体类型。这不仅是编写无错代码的技巧,更是构建健壮、可预测应用的思维模式。


在本章中,我们将循序渐进,像侦探一样,学会如何从模糊的可能性中推断出确切的真相:

  1. 首先,我们将直面 联合类型的操作困境,体会为什么需要类型收窄。
  2. 接着,我们将掌握最基础的守护工具 typeof,解决原始类型的判断问题。
  3. 然后,我们将学习使用 in 操作符,来安全地区分拥有不同属性的对象。
  4. 最后,我们将打造自己的终极武器——自定义类型守护,将复杂的类型判断逻辑封装成可复用的“认证函数”。

3.1. 痛点呈现:联合类型的操作困境

在上一章,我们学会了使用联合类型来增强代码的灵活性。例如,一个函数可能接收一个 string 或者一个 number。但这种灵活性也带来了新的问题。

场景复现:我们想编写一个函数 printValue,如果传入的是字符串,就打印它的大写形式;如果传入的是数字,就打印它保留两位小数的形式。

问题代码示例:

1
2
3
4
5
6
7
8
9
10
11
function printValue(value: string | number) {
// 🔴 编译错误!
// 属性“toUpperCase”在类型“string | number”上不存在。
// 属性“toUpperCase”在类型“number”上不存在。
console.log(value.toUpperCase());

// 🔴 同样会编译错误!
// 属性“toFixed”在类型“string | number”上不存在。
// 属性“toFixed”在类型“string”上不存在。
console.log(value.toFixed(2));
}

核心问题: 在 TypeScript 对 value 进行类型检查时,它只能保证这个变量是 string number 中的一种。因此,任何只在其中一个类型上存在的方法(如 toUpperCasetoFixed),都不能被安全调用。

为了解决这个问题,我们需要一种机制,能在函数内部的特定代码块中,让 TypeScript “确信”这个变量此刻的具体类型。这个过程,就叫做 类型收窄


3.2. 类型守护 I:typeof

解决方案: 对于原始数据类型(string, number, boolean, symbol, bigint, undefined),最简单直接的类型守护就是使用 JavaScript 的 typeof 操作符。

重构代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function printValue(value: string | number): void {
// 使用 typeof 进行类型守护
if (typeof value === "string") {
// 在这个 if 代码块内,TypeScript 已经智能地将 value 的类型收窄为 `string`
// 鼠标悬停在下面的 `value` 上,你会看到它的类型就是 string
console.log(value.toUpperCase()); // ✅ 正确!
} else {
// 在 else 代码块内,TypeScript 推断出 value 的类型必定是剩下的 `number`
console.log(value.toFixed(2)); // ✅ 正确!
}
}

printValue("hello world");
printValue(123.456);

typeof 是处理原始类型联合类型的首选工具,它简单、高效,并且利用了 JavaScript 的原生能力。


3.3. 类型守护 II:in 操作符

typeof 对原始类型非常有效,但当我们面对不同的对象类型时,它就无能为力了(因为 typeof 对任何对象(null 除外)都只会返回 "object")。

痛点呈现:在我们的 Todo 应用中,假设除了 Todo,还有一种 Note 类型。我们需要一个函数来处理这两种类型的对象。

场景代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 在 `src/types.ts` 中可以补充这些类型
interface Todo {
id: number;
text: string;
completed: boolean;
// Todo 特有的方法
toggle(): void;
}

interface Note {
id: number;
content: string;
// Note 特有的属性
color: 'yellow' | 'blue' | 'green';
}

type ListEntry = Todo | Note;

function processEntry(entry: ListEntry) {
// 🔴 编译错误!属性 `toggle` 在类型 `Note` 上不存在
entry.toggle();

// 🔴 编译错误!属性 `color` 在类型 `Todo` 上不存在
console.log(`Color is: ${entry.color}`);
}

解决方案: 使用 in 操作符。in 操作符可以检查一个对象自身或其原型链上是否存在某个属性。TypeScript 能够理解这个检查,并据此收窄类型。

重构代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function processEntry(entry: ListEntry): void {
console.log(`Processing entry #${entry.id}`);

// 使用 in 操作符进行类型守护
if ("completed" in entry) {
// 在这个块内,TypeScript 知道 entry 的类型是 Todo
// 因为只有 Todo 类型拥有 "completed" 属性
console.log(`Todo item: ${entry.text}`);
// entry.toggle(); // 如果 toggle 方法已实现,这里就可以安全调用
} else {
// 在这个块内,TypeScript 推断 entry 的类型必定是剩下的 Note
console.log(`Note with color ${entry.color}: ${entry.content}`);
}
}

3.4. 终极武器:自定义类型守护 (is)

in 操作符很棒,但如果我们的判断逻辑比较复杂,或者需要在多个地方重复使用,那么将这个逻辑封装起来会是更好的选择。

解决方案: 创建一个返回类型为 类型谓词 (parameterName is Type) 的函数。这种函数就是自定义类型守护。

重构 processEntry 的例子:

第一步:创建一个自定义类型守护函数

1
2
3
4
5
6
// 这个函数返回一个布尔值,但它的类型签名非常特殊
// `entry is Todo` 就是类型谓词
// 它告诉 TypeScript:如果这个函数返回 true,那么你就可以确信传入的 `entry` 参数是 `Todo` 类型
function isTodo(entry: ListEntry): entry is Todo {
return (entry as Todo).completed !== undefined;
}

第二步:在我们的逻辑中使用它

1
2
3
4
5
6
7
8
9
10
11
function processEntryWithCustomGuard(entry: ListEntry): void {
console.log(`Processing entry #${entry.id}`);

// 使用自定义类型守护函数
if (isTodo(entry)) {
// 在这个块内,TypeScript 同样能将 entry 的类型收窄为 Todo
console.log(`Todo item: ${entry.text}`);
} else {
console.log(`Note with color ${entry.color}: ${entry.content}`);
}
}

思维转变: 自定义类型守护,让我们将业务逻辑(“如何判断一个东西是 Todo”)与类型系统进行了关联。这使得我们的代码不仅更安全,而且 可读性可复用性 大大增强,代码本身就在“述说”它的意图。


3.5. 本章核心速查总结

守护方式核心用途适用场景
typeof检查原始数据类型当联合类型包含 string, number, boolean 等时。
in检查对象上是否存在属性(推荐) 当需要区分不同形状的接口 (interface) 或对象类型时。
自定义 (is)封装可复用的类型判断逻辑当类型判断逻辑复杂,或需要在多处重复使用时。

3.6. 高频面试题与陷阱

面试官深度追问
2025-09-03

请解释一下什么是 TypeScript 的“类型收窄”,以及你通常用哪些方法来实现它?

类型收窄是指在特定的代码分支中,TypeScript 编译器能够根据上下文,将一个宽泛的类型(如联合类型)推断为一个更具体的类型的过程。这是在 strict: true 模式下保证类型安全的关键。我主要使用三种方法:1. typeof 用于判断原始类型;2. in 操作符用于判断对象上是否存在某个唯一的属性;3. 自定义类型守护函数(使用 is 谓词),用于封装可复用的、更复杂的判断逻辑。

很好。那么,在什么情况下你必须选择使用自定义类型守护,而不是简单地使用 typeofin

当类型判断的依据不是单一的属性或类型时。例如,一个对象可能需要同时满足多个条件才能被确认为某个特定类型,或者判断逻辑涉及到复杂的计算。将这些复杂的逻辑封装在一个 is 函数中,可以让业务代码更清晰,也便于统一维护和测试这个判断逻辑本身。