请求状态建模
让成功与失败数据形成合法配对,排除缺失信息和交叉组合。
本章目录CONTENTS ↓
请求成功必须携带用户数据,失败必须携带错误信息。目标是在类型层面排除不完整状态,并让消费代码通过状态检查访问正确的数据。
需要的基础是联合成员与类型收窄;本章重点在选择业务模型以及比较它允许的状态。
用联合类型表达合法状态
同一个请求会成功或失败,但两种状态需要的数据不同。如果各字段都独立可选,就会允许业务上不完整的状态。可辨识联合把每一种合法配对写成一个成员,让类型检查能够排除不合法的组合。
当字段确实彼此独立、可以单独缺省时,可选属性仍然合适;当状态决定哪些数据必须存在时,适合使用这里的联合模型。
先看模型允许了什么
请求成功必须有用户数据,失败必须有错误信息。下面的类型却允许只有状态、没有数据的对象。
type User = { name: string };
type LooseResult = {
ok: boolean;
data?: User;
error?: string;
};
const incomplete: LooseResult = { ok: true }; // 类型允许即使检查了 ok 为真,data 仍可能不存在。报错提示的是模型没有表达成功状态与数据的关联。
把每一种合法状态写成一个成员
type Result =
| { ok: true; data: User }
| { ok: false; error: string };
function display(r: Result): string {
if (r.ok) {
return r.data.name;
}
return r.error;
}ok 是区分成员的字面量属性。判断它时,整个 r 被收窄,对应的必填数据也随之确定。
两个独立联合不会自动建立配对
type LooseMessage = {
kind: "login" | "progress";
data: { userId: string } | { percent: number };
};
const crossed: LooseMessage = {
kind: "login", data: { percent: 60 }
}; // 可以赋值,但违背业务配对
type Message =
| { kind: "login"; data: { userId: string } }
| { kind: "progress"; data: { percent: number } };前者分别允许两种 kind 与两种 data 形状,因此包含交叉配对;后者把合法配对写在各成员内部。
如何判断模型是否合适
检查成功缺少数据、失败缺少错误、状态与数据交叉这三种情况是否被类型拒绝。合法状态分别写成联合成员后,消费代码才能通过可辨识属性安全访问相应数据。这里的检查针对静态类型,外部返回的数据仍需要合适的运行时处理。
官方阅读
- Discriminated unions:按标签区分完整成员。
这一页,先记到这里。