学径XUEJING · 个人学习平台通知设置

课程库 TypeScript 状态机 · 把「乱」写成「图」 › 第 2

判别联合 —— 让每个状态携带自己的数据

普通课50 分钟

本课导读

上一课收纳了状态,留了个尾巴:success 的数据、error 的错误信息放哪?单独的 data 变量放外面,又回到"靠默契"的老路——谁保证 error 时 data 是空的?本课的主角判别联合(discriminated union)一次解决:把状态和它专属的数据焊在同一个对象形状里,用一个公共字段(判别字段)区分是哪种形状。这是 TS 处理"多种情况各带各的货"的官方推荐姿势,也是整个状态机单元的类型地基。顺便正式引入 switch——判别联合的最佳拍档。预计 50 分钟。

说明

前置工具箱:unit-1 第 4 课的 interface(描述形状)、第 5 课的字面量联合与收窄(判别联合=两者的合体技)。本课的收窄不用 in 摸字段,改用更利落的判别字段——学完你会发现 in 的出场机会大幅减少。

从"数据放外面"到"数据焊进状态"

先看"靠默契"版的问题:

TypeScript

注意那个 data! 里的感叹号——非空断言,翻译成人话是"编译器你闭嘴,我保证有值"。每个 ! 都是一份手写的默契,而上一课已经证明过默契的下场。

判别联合的思路:既然 success 必有 data、error 必有 message,那就让"success"这个状态本身长着 data 字段

TypeScript

读一遍这个类型定义,它就是上一课那句原则的完全体:success 没带数据、loading 硬塞数据、error 拿错行李——全部在编译期灭绝。没有 !,没有默契,没有"记得清空"。

判别字段:一摸就知道是谁

四个形状共享一个 status 字段,且每个形状里它是不同的字面量——这样的字段叫判别字段(discriminant)。它的魔力在收窄时显现:

TypeScript

对比 unit-1 用 in 摸字段的写法("有 email 字段的大概是邮件联系人"),判别字段是自报家门:"我 status 是 success"——一次比较,整个形状确定,所有专属字段解锁。这就是它成为官方推荐的原因:意图更清楚,收窄更利落,形状再多也不乱。

switch:判别联合的最佳拍档

分支多起来之后,一串 if 不如一个 switch 干净。switch 是 JS 老功能,本课正式引入——TS 对它的判别收窄支持得同样好:

TypeScript

三个细节:

  • 每个 case 内部自动收窄——case "success" 里 state.data 直接用
  • 每个 case 用 return 结束,就不需要 break(switch 的经典陷阱"穿透"也就不存在)
  • 不写 default 是故意的:四种情况全覆盖时,TS 知道函数必有返回值。更妙的是——如果哪天类型里加了第五种状态而这里忘了处理,"函数可能没有返回值"的编译错误会立刻找上门。穷尽检查这个大杀器,第 3 课正式配发

提示

命名判别字段的行规:statuskindtypetag 都常见。同一个项目统一用一个就好。顺带一提:学径课程数据里的 type: "normal" | "tool" | "project" 就是判别字段的真实应用——你每天点开的课程列表,背后正是一个判别联合。

事件也是判别联合

状态用判别联合,事件(下一课的主角)同样用——不带货的事件是纯字面量,带货的事件带上它的载荷:

TypeScript

状态和事件都定型了,下一课就能写出状态机的心脏:(状态, 事件) => 新状态 的转移函数。

✍️ 练习

练习 1:播放器状态改造

把上一课练习 1 的播放器升级成判别联合:stopped 无附加数据;playingpaused 都带 track: string(曲名)和 position: number(秒);bufferingtrack: string。写 describe(state) 函数用 switch 返回一句描述(如"正在播放:晴天(第 35 秒)")。

TypeScript
✅ 参考答案
type PlayerState =
    | { status: "stopped" }
    | { status: "playing"; track: string; position: number }
    | { status: "paused"; track: string; position: number }
    | { status: "buffering"; track: string };

function describe(state: PlayerState): string {
    switch (state.status) {
        case "stopped":
            return "未在播放";
        case "playing":
            return "正在播放:" + state.track + "(第 " + state.position + " 秒)";
        case "paused":
            return "已暂停:" + state.track + "(第 " + state.position + " 秒)";
        case "buffering":
            return "缓冲中:" + state.track;
    }
}

console.log(describe({ status: "playing", track: "晴天", position: 35 }));
console.log(describe({ status: "stopped" }));

{ status: "stopped", track: "晴天" } 会报 TS2353——stopped 不认识 track。注意 playing 和 paused 形状相同也要分开写:判别的依据是 status 的字面量,不是字段组合。

练习 2:购物车事件集

给购物车定义事件判别联合 CartEventADDitemId: stringqty: numberREMOVEitemId: stringCLEAR 光杆;CHECKOUT 光杆。写 log(event) 用 switch 返回日志句子。然后思考题:为什么 ADD 的 qty 不设计成"可选、默认为 1"?(提示:上一课的默契问题。)

TypeScript
✅ 参考答案
type CartEvent =
    | { type: "ADD"; itemId: string; qty: number }
    | { type: "REMOVE"; itemId: string }
    | { type: "CLEAR" }
    | { type: "CHECKOUT" };

function log(event: CartEvent): string {
    switch (event.type) {
        case "ADD":
            return "加入商品 " + event.itemId + " × " + event.qty;
        case "REMOVE":
            return "移除商品 " + event.itemId;
        case "CLEAR":
            return "清空购物车";
        case "CHECKOUT":
            return "发起结算";
    }
}

console.log(log({ type: "ADD", itemId: "sku-1", qty: 2 }));
console.log(log({ type: "CLEAR" }));

思考题:可选 + 默认值意味着每个消费 ADD 事件的地方都要记得"没传就是 1"——又一份手写默契;显式必填则把决定权收到事件创建的唯一入口。载荷字段的原则和状态一致:要么明确带、要么明确不带,别"可能带"

📝 随堂测验

随堂测验0 / 5 题正确

1. '数据放在状态旁边'(独立的 data、errorMsg 变量)的问题是?

2. 判别字段(discriminant)的定义是?

3. 对比 in 收窄,判别字段收窄的优势是?

4. switch 处理判别联合时'每个 case 用 return、不写 default',各图什么?

5. playing 和 paused 形状完全相同(都带 track 和 position),为什么仍要分开写两个形状?

本课小结

  • 判别联合 = interface(形状)+ 字面量联合(判别字段)的合体技:每种状态一个形状,公共字段自报家门
  • 数据焊进状态:success 必带 data、loading 带不了 data——默契升级为类型,非空断言 ! 消失
  • 判别收窄:state.status === "success" 一次比较解锁全部专属字段;比 in 摸字段更清楚
  • switch 是最佳拍档:case 内自动收窄、return 代替 break、不写 default 给穷尽检查留门
  • 事件同样用判别联合:光杆事件纯字面量对象,带货事件焊上载荷

状态定型了、事件定型了,下一课写状态机的心脏:转移函数 (state, event) => state——以及它的两种写法之争:switch 派与查表派。

划选正文任意文字可高亮、批注或加入复习卡

讨论

载入中…