3. 用类型别名与联合类型处理多形态API响应

用类型别名与联合类型处理多形态API响应 学习卡片

为什么多形态响应需要联合类型

一个 API 请求通常有三种结果:加载中、成功、失败。如果用一个宽泛的接口描述,所有字段都变成可选,调用方就必须反复做非空判断,类型系统帮不上忙。更安全的做法是:为每种形态定义独立的类型别名,再用联合类型把它们组合起来,最后通过一个公共的 status 字段做判别(discriminated union)。

ApiResponse<T> 联合类型判别结构 type ApiResponse<T> = Loading | Success | Failure LoadingState status: "loading" progress: number 无 data / error 字段 SuccessState<T> status: "success" data: T 携带业务数据 FailureState status: "failure" error: string 携带错误信息 判别字段 status 收窄类型 switch (res.status) 每个分支只暴露该状态下真实存在的字段,访问 data 或 error 前必须先通过 status 收窄

定义三种状态类型别名

先分别定义三种形态,再用类型别名组合。注意每个分支的 status 使用字面量类型(如 "success"),这是联合类型能够被 TypeScript 自动收窄的关键。

// 加载中:只有进度,没有数据
type LoadingState = {
  status: "loading";
  progress: number; // 0 ~ 100
};

// 成功:携带泛型数据
type SuccessState<T> = {
  status: "success";
  data: T;
};

// 失败:携带错误信息
type FailureState = {
  status: "failure";
  error: string;
};

// 联合类型:三种形态之一
type ApiResponse<T> = LoadingState | SuccessState<T> | FailureState;

编写安全访问函数

下面的函数接收 ApiResponse<T>,通过 switch (res.status) 让 TypeScript 在每个 case 中自动收窄类型。在 "success" 分支里可以直接访问 res.data,在 "failure" 分支里可以直接访问 res.error,无需任何类型断言。

function renderResponse<T>(res: ApiResponse<T>): string {
  switch (res.status) {
    case "loading":
      // 此处 res 被收窄为 LoadingState
      return `加载中... ${res.progress}%`;

    case "success":
      // 此处 res 被收窄为 SuccessState<T>
      return `请求成功,数据:${JSON.stringify(res.data)}`;

    case "failure":
      // 此处 res 被收窄为 FailureState
      return `请求失败:${res.error}`;

    default:
      // 穷尽检查:若新增状态未处理,此处会报错
      const _exhaustive: never = res;
      return _exhaustive;
  }
}
最后的 default 分支使用 never 做穷尽性检查。如果未来给联合类型新增了状态却忘记处理,编译阶段就会报错,而不是等到运行时才发现遗漏。

验证类型收窄效果

用三个示例对象分别调用函数,观察输出。这些是教学示例数据,用于验证类型收窄逻辑,并非真实接口返回。

// 示例数据(教学用,非真实接口返回)
const loading: ApiResponse<string[]> = { status: "loading", progress: 42 };
const success: ApiResponse<string[]> = { status: "success", data: ["a", "b"] };
const failure: ApiResponse<string[]> = { status: "failure", error: "网络超时" };

console.log(renderResponse(loading)); // 加载中... 42%
console.log(renderResponse(success)); // 请求成功,数据:["a","b"]
console.log(renderResponse(failure)); // 请求失败:网络超时

可以尝试故意写错来验证类型保护:在 "loading" 分支里访问 res.data,或在 "success" 分支里访问 res.error,TypeScript 都会在编译期报错,提示该属性在当前类型上不存在。这正是联合类型相比“全可选字段接口”的核心优势。

不要用类型断言(as)绕过收窄。如果发现需要断言才能访问字段,通常说明联合类型的分支定义或判别字段设计有问题,应优先修正类型定义。