如何在 TypeScript 中为 API 响应添加类型
每个前端开发者都遇到过这种情况:你从 API 获取数据,访问某个属性,却在运行时得到 undefined —— 即使 TypeScript 从未报错。问题不在于你的代码,而在于 TypeScript 的类型系统只在编译时运行。它无法知道你的 API 实际返回了什么。
本文介绍了在 TypeScript 中为 API 响应添加类型的实用模式,从定义基本接口到运行时验证和基于契约的类型生成。
核心要点
- TypeScript 的类型系统仅在编译时运行 —— 它无法在运行时验证 API 返回数据的实际结构。
- 使用接口或类型别名配合可辨识联合类型来定义清晰的预期响应结构。
- 泛型 fetch 包装器可以保持调用代码的简洁性,并使预期类型更加明确。
- 对于不可信或外部数据,使用运行时验证库(如 Zod、Valibot 或 ArkType)来弥合编译时类型与实际响应之间的差距。
- 当存在 OpenAPI 规范时,从中生成类型以保持前端和后端契约的同步。
为什么 TypeScript 无法在运行时保护你
当你调用 response.json() 时,TypeScript 通常将结果视为 unknown(或者根据环境类型定义,有时是 any)。这意味着你可以将其转换为任何你想要的类型 —— 编译器会完全信任你。
// ❌ TypeScript 盲目信任这个类型转换
const data = (await response.json()) as User
console.log(data.email) // 运行时可能是 undefined
这是需要理解的核心边界:TypeScript 的 API 响应类型为你提供编译时安全性,但它们不会验证 API 实际返回的 JSON。 如果结构发生变化,TypeScript 不会知道。
为预期响应结构定义类型
第一步是为你的预期数据赋予结构。使用接口或类型别名来描述响应结构:
interface User {
id: string
name: string
email: string
}
对于一致的 API,通用包装器可以简洁地处理成功和错误状态:
type ApiResponse<T> =
| { status: "ok"; data: T }
| { status: "error"; message: string }
这种可辨识联合类型模式比包含许多可选字段的单一类型更加透明。当你检查 status 时,TypeScript 会自动收窄类型 —— 无需额外的类型守卫。
使用泛型包装器为 Fetch 响应添加类型
在 TypeScript 中为 fetch 响应添加类型,使用可复用的辅助函数会更简洁:
async function apiFetch<T>(url: string): Promise<T> {
const response = await fetch(url)
if (!response.ok) throw new Error(`HTTP error: ${response.status}`)
return response.json() as Promise<T>
}
// 使用方式
const user = await apiFetch<User>("/api/users/1")
这样可以保持调用代码的简洁,同时使预期结构更加明确。同样的模式也适用于 Axios 或其他任何 HTTP 客户端。
Discover how at OpenReplay.com.
运行时验证:弥合差距
像 as User 这样的类型断言是对编译器的承诺,而不是保证。对于不可信的外部数据 —— 第三方 API、用户生成的内容,或任何你无法完全控制的响应 —— 你需要运行时验证。
Zod 是使用最广泛的选择。你定义一个模式(schema),解析响应,并获得完全类型化的结果:
import { z } from "zod"
const UserSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
})
type User = z.infer<typeof UserSchema> // 从 schema 派生
const data = await response.json()
const user = UserSchema.parse(data) // 如果结构错误则抛出异常
关键优势:你的 TypeScript 类型和运行时验证保持同步,因为它们源自同一个真实来源。
值得了解的替代方案:
当你想在数据无效时抛出异常时使用 .parse(),或者当你想优雅地处理错误而不抛出异常时使用 .safeParse()。
使用 OpenAPI 的契约驱动类型
如果你的 API 有 OpenAPI 规范,你可以使用 openapi-typescript 自动生成 TypeScript 类型:
npx openapi-typescript ./api-spec.yaml -o ./types/api.d.ts
这种方法使你的类型与 API 文档保持同步。它与运行时验证是互补的 —— 生成的类型处理编译时层面,而 Zod 或 Valibot 处理运行时层面。
选择正确的方法
| 场景 | 推荐模式 |
|---|---|
| 你控制的内部 API | 接口 + 类型断言 |
| 与后端共享契约 | OpenAPI 生成的类型 |
| 第三方或不可信 API | Zod/Valibot schema 验证 |
| 需要双层安全保障 | 生成的类型 + 运行时 schema |
总结
TypeScript 为你提供结构和自动补全,但它无法验证通过网络传输的数据。对于你信任的 API,定义良好的接口和类型化的 fetch 包装器就足够了。对于外部或不可预测的数据,将你的类型与像 Zod 这样的运行时验证器配对使用。当你有 API 规范时,从中生成类型。这些方法并不冲突 —— 它们结合使用效果最佳。
常见问题
不安全。'as' 关键字是类型断言,而不是运行时检查。它告诉编译器将值视为特定类型,但不会验证实际数据。如果 API 响应结构与你断言的不同,你会遇到 TypeScript 无法捕获的运行时错误。
当你使用来自不完全受控来源的数据时,例如第三方 API 或公共端点,应使用 Zod 或类似的运行时验证库。如果你同时拥有前端和后端,并且对响应结构有很强的信心,那么带有类型断言的简单接口通常就足够了。
可以,而且推荐这种组合以获得最大的安全性。OpenAPI 生成的类型为你提供编译时的自动补全和类型检查,而 Zod schema 在运行时验证实际的响应数据。它们共同覆盖了类型安全的两个层面。
Fetch API 的 response.json() 方法在 TypeScript 中默认返回 Promise<any>。这意味着除非你明确断言或验证,否则编译器不会对结果强制执行任何类型检查。一些团队会用更严格的类型定义覆盖它,使其返回 unknown,从而强制进行显式验证。
Complete picture for complete understanding
Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.
Check our GitHub repo and join the thousands of developers in our community.