前后端对接时,接口响应结构需要统一约定:成功时返回业务数据,失败时返回错误信息且不带 data。用 TypeScript 把这套约定建模为"成功/失败的联合类型",并配合 success 字面量字段做可辨识联合,可以在调用方用 if 收窄类型,自动区分有 data 与无 data 两种形态,杜绝访问空 data 的隐患。
// 统一 API 响应类型:成功与失败的联合,data 泛型参数化
// 用 success 字面量字段做可辨识联合,便于类型收窄
// 成功响应:携带 data
interface SuccessResponse<T> {
code: number;
message: string;
data: T;
success: true; // 字面量类型,判别字段
}
// 错误响应:无 data
interface ErrorResponse {
code: number;
message: string;
data: null;
success: false;
}
// 联合:一个响应要么成功要么失败
type ApiResponse<T> = SuccessResponse<T> | ErrorResponse;
// 应用:请求函数返回联合类型,调用方用 success 收窄
async function request<T>(url: string): Promise<ApiResponse<T>> {
const res = await fetch(url).then(r => r.json());
return res as ApiResponse<T>;
}
interface User { id: number; name: string; }
// 调用方:用 success 字段做类型收窄
async function main() {
const res = await request<User>('/api/user/1');
if (res.success) {
// 此处 res 收窄为 SuccessResponse<User>
console.log(res.data.name); // 合法:data 是 User
} else {
// 此处 res 收窄为 ErrorResponse
console.log(res.message); // 合法:错误信息
// res.data.name; // 报错:data 为 null
}
}
success: true / false 字面量字段作为"判别字段",TS 能根据该字段在 if 分支中自动收窄到对应成员。SuccessResponse<T>:data 类型由泛型 T 参数化,不同接口传入不同 T,复用同一套响应结构。null,收窄后访问 data.name 会报错,从类型层面防止空指针。A | B 而非 data?: T,因为成功必有 data、失败必无 data,联合比可选更精确地表达"二者择一"。ApiResponse<T>,调用方处理逻辑一致,错误处理集中、类型安全。// 不同接口复用同一响应结构
type UserResp = ApiResponse<User>;
type ListResp = ApiResponse<User[]>;
// 封装一个统一的"取 data 或抛错"工具
function unwrap<T>(res: ApiResponse<T>): T {
if (res.success) return res.data;
throw new Error(res.message);
}
const u = unwrap({ code: 0, message: 'ok', data: { id: 1, name: 'wjs' }, success: true });
// => u: User
// 类型守卫函数:便于在多处复用收窄逻辑
function isSuccess<T>(res: ApiResponse<T>): res is SuccessResponse<T> {
return res.success;
}
// 用 if (isSuccess(res)) 替代 if (res.success),逻辑更内聚