3. 用类型别名与联合类型处理多形态API响应
为什么多形态响应需要联合类型
一个 API 请求通常有三种结果:加载中、成功、失败。如果用一个宽泛的接口描述,所有字段都变成可选,调用方就必须反复做非空判断,类型系统帮不上忙。更安全的做法是:为每种形态定义独立的类型别名,再用联合类型把它们组合起来,最后通过一个公共的 status 字段做判别(discriminated union)。
定义三种状态类型别名
先分别定义三种形态,再用类型别名组合。注意每个分支的 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)绕过收窄。如果发现需要断言才能访问字段,通常说明联合类型的分支定义或判别字段设计有问题,应优先修正类型定义。