TYPESCRIPT / PRACTICE

请求状态建模

让成功与失败数据形成合法配对,排除缺失信息和交叉组合。

本章目录CONTENTS ↓

请求成功必须携带用户数据,失败必须携带错误信息。目标是在类型层面排除不完整状态,并让消费代码通过状态检查访问正确的数据。

需要的基础是联合成员与类型收窄;本章重点在选择业务模型以及比较它允许的状态。

用联合类型表达合法状态

同一个请求会成功或失败,但两种状态需要的数据不同。如果各字段都独立可选,就会允许业务上不完整的状态。可辨识联合把每一种合法配对写成一个成员,让类型检查能够排除不合法的组合。

当字段确实彼此独立、可以单独缺省时,可选属性仍然合适;当状态决定哪些数据必须存在时,适合使用这里的联合模型。

先看模型允许了什么

请求成功必须有用户数据,失败必须有错误信息。下面的类型却允许只有状态、没有数据的对象。

ts
type User = { name: string };
type LooseResult = {
  ok: boolean;
  data?: User;
  error?: string;
};

const incomplete: LooseResult = { ok: true }; // 类型允许

即使检查了 ok 为真,data 仍可能不存在。报错提示的是模型没有表达成功状态与数据的关联。

把每一种合法状态写成一个成员

ts
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 被收窄,对应的必填数据也随之确定。

两个独立联合不会自动建立配对

ts
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 形状,因此包含交叉配对;后者把合法配对写在各成员内部。

如何判断模型是否合适

检查成功缺少数据、失败缺少错误、状态与数据交叉这三种情况是否被类型拒绝。合法状态分别写成联合成员后,消费代码才能通过可辨识属性安全访问相应数据。这里的检查针对静态类型,外部返回的数据仍需要合适的运行时处理。

官方阅读

这一页,先记到这里。