在前后端分离架构里,服务端通常不会直接把业务数据裸返回,而是包一层包含code、message与data的结构。TypeScript若仅把code声明为number,前端就丧失了在编译阶段识别非法状态码的能力。类型级编程允许我们把状态码视作类型而非值,通过联合字面量、泛型与条件类型,在类型层面完成校验与推导,使API响应包装器具备自解释的安全边界。

类型级状态码的底层表达与字面量联合
类型级编程的核心是把运行期可能出现的值映射到类型空间。对于API包装器,常见的状态码如200表示成功、401表示未授权、500表示服务器异常。我们可以定义一个状态码字面量联合类型,而不是宽泛的number。这样任何把code赋值为不存在的数字的行为,都会在类型检查时被拒绝。
使用字面量联合后,配合泛型参数,我们能够让包装器类型记录具体响应的状态码。例如定义ApiCode为200 | 401 | 500,再定义ApiResponse<T, C extends ApiCode>。调用时若传入C = 200,则data字段被推断为T;若传入401,可借助条件类型把data限制为null。这种约束不消耗运行性能,全部发生在编译期。
与直接使用枚举相比,字面量联合更轻量且易于被TypeScript自动推断。枚举在编译后仍是对象,而联合类型在声明文件里更透明。下面的代码展示了基础定义方式:
type ApiCode = 200 | 401 | 500;
type ApiResponse<T, C extends ApiCode> = {
code: C;
message: string;
data: C extends 200 ? T : null;
};
function ok<T>(data: T): ApiResponse<T, 200> {
return { code: 200, message: 'success', data };
}
function fail(): ApiResponse<null, 401> {
return { code: 401, message: 'unauthorized', data: null };
}
用条件类型与映射类型关联状态码与消息
仅靠状态码联合还不够,实际项目中不同code往往对应不同提示文案。类型级编程可以通过映射类型建立code到message的字典,再利用索引访问类型取出对应字符串。这样当开发者写错message时,编译器能根据code指出正确内容。
我们可以声明一个CodeMessageMap接口,把每个状态码映射到固定文案。接着定义类型MessageOf<C>等于CodeMessageMap[C]。在包装器类型中,把message字段类型设为MessageOf<C>,就实现了状态码与消息的绑定。若后期新增503,只需在映射表和联合类型里同步添加,旧代码漏改会立刻报错。
更进一步,可以用模板字面量类型拼出更复杂的返回结构。例如把code与message合并成一种事件名类型,方便做事件总线分发。下方示例演示映射与索引访问的组合:
interface CodeMessageMap {
200: 'success';
401: 'unauthorized';
500: 'internal_error';
}
type MessageOf<C extends ApiCode> = CodeMessageMap[C];
type StrictResponse<T, C extends ApiCode> = {
code: C;
message: MessageOf<C>;
data: C extends 200 ? T : null;
};
const res: StrictResponse<string, 200> = {
code: 200,
message: 'success',
data: 'hello'
};
在真实请求封装中应用类型级状态码校验
把上述类型用到fetch或axios封装里,可以让调用方获得精确提示。我们定义一个request函数,根据传入的期望状态码泛型,返回对应data类型。若接口实际返回了未声明的code,我们可以在运行时抛错,但类型上已经禁止了错误分支的静态拼接。
实践中建议把后端开放的状态码收敛到同一个联合类型文件,前端所有包装器都引用它。配合类型守卫函数,如isSuccess(res)利用res is ApiResponse<T, 200>断言,能在if块内自动收窄data为非空。这样业务组件无需写冗余判断,也不会因拼错code导致打包后逻辑偏离。
下面给出一个请求封装的简化例子,展示类型级编程如何贯穿调用链:
declare function fetchJson(url: string): Promise<StrictResponse<unknown, ApiCode>>;
async function getUser() {
const r = await fetchJson('/api/user');
if (r.code === 200) {
// r.data 在此处为 unknown,需进一步断言
const name = (r.data as { name: string }).name;
return name;
}
// 其他 code 下 r.data 为 null
throw new Error(r.message);
}
类型级编程不是炫技,而是把后端契约前移到编译期。当团队维护的接口增多,这种写法能显著减少因为状态码误用引发的线上故障,也让新成员在阅读类型定义时快速理解系统边界。
TypeScript类型级编程API响应包装器修改时间:2026-08-15 10:21:26