大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
异步与 API 类型
这一课你将能做到
- 给
Promise、async/await函数标注正确类型,处理Promise<T>与Promise<void> - 用判别联合建模 API 响应,写出类型安全的 fetch 封装
- 用 zod 做运行时校验,弥补 TS「编译期类型」在运行时的盲区
为什么要学
前面学的所有 TS 知识,最终都要落在「和后端打交道」上。而异步操作是前端最不可靠的环节:网络会超时、后端会返回意料之外的数据、Promise 会 reject、JSON 解析会出错。
TS 的类型系统有个根本局限:类型只在编译期存在。interface User { name: string } 在编译后消失得无影无踪。如果后端返回 { name: 123 },TS 编译时不会报错,运行时你的代码就崩了。
所以异步 API 开发要双保险:编译期用 TS 类型建模,运行时用 zod 校验。这一课把两条线都打通。
先建立图像
异步类型的核心模型:
Promise<T> = 未来某个时刻会产出类型 T 的值
async function fetchUser(): Promise<User>
│
▼
await 之后得到 User 类型
│
▼
但运行时可能返回任何数据
│
▼
需要 zod 在运行时校验「这真的是 User 吗?」
跟着做
新建 async-demo.ts,逐步实操。
步骤 1:Promise 类型基础
// Promise 的泛型参数表示 resolve 出来的值的类型
function delay(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function fetchNumber(): Promise<number> {
return new Promise((resolve) => {
setTimeout(() => resolve(42), 100);
});
}
// async 函数的返回值自动包装成 Promise<T>
async function getUserName(): Promise<string> {
await delay(100);
return "小明";
}
// 使用
async function main() {
const num = await fetchNumber(); // num: number
const name = await getUserName(); // name: string
console.log(num, name); // 42 "小明"
}
main();
步骤 2:Promise 的错误处理
interface User {
id: number;
name: string;
}
async function fetchUser(id: number): Promise<User> {
const response = await fetch(`https://api.example.com/users/${id}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json(); // 返回 any,需要处理
}
// 调用方用 try/catch 捕获
async function loadUser() {
try {
const user = await fetchUser(1);
console.log(user.name);
} catch (error) {
// error 默认是 unknown 类型(TS 4.4+)
if (error instanceof Error) {
console.error("加载失败:", error.message);
}
}
}
步骤 3:判别联合建模 API 响应
// API 响应的两种形态
interface ApiSuccess<T> {
code: 0;
data: T;
message: "success";
}
interface ApiError {
code: 400 | 401 | 403 | 500;
error: string;
}
type ApiResult<T> = ApiSuccess<T> | ApiError;
// 类型守卫
function isSuccess<T>(result: ApiResult<T>): result is ApiSuccess<T> {
return result.code === 0;
}
// 使用
async function handleUserResponse(result: ApiResult<User>) {
if (isSuccess(result)) {
// 这里 result 是 ApiSuccess<User>,data 存在
console.log(`欢迎 ${result.data.name}`);
} else {
// 这里 result 是 ApiError,error 存在
console.error(`失败:${result.error}`);
}
}
步骤 4:类型安全的 fetch 封装
class ApiClient {
private baseUrl: string;
constructor(baseUrl: string) {
this.baseUrl = baseUrl;
}
async get<T>(path: string): Promise<T> {
const response = await fetch(`${this.baseUrl}${path}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json() as Promise<T>;
}
async post<T, R>(path: string, body: T): Promise<R> {
const response = await fetch(`${this.baseUrl}${path}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json() as Promise<R>;
}
}
// 使用
const api = new ApiClient("https://api.example.com");
interface CreateUserRequest {
name: string;
email: string;
}
interface CreateUserResponse {
id: number;
createdAt: string;
}
async function demo() {
const user = await api.get<User>("/users/1");
console.log(user.name);
const created = await api.post<CreateUserRequest, CreateUserResponse>(
"/users",
{ name: "小红", email: "hong@example.com" }
);
console.log(created.id);
}
问题:response.json() as Promise<T> 是类型断言,不是运行时校验。如果后端返回的数据结构变了,TS 不会发现。
步骤 5:zod 运行时校验
安装:
npm install zod
使用:
import { z } from "zod";
// 1. 定义 schema(运行时存在的校验规则)
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
age: z.number().int().positive().optional(),
});
// 2. 从 schema 推导类型(和手写 interface 完全等价)
type User = z.infer<typeof UserSchema>;
// { id: number; name: string; email: string; age?: number }
// 3. 运行时校验
function parseUser(data: unknown): User {
return UserSchema.parse(data); // 不符合就抛 ZodError
}
// 安全版本:不抛异常,返回结果对象
function safeParseUser(data: unknown) {
const result = UserSchema.safeParse(data);
if (result.success) {
console.log("校验通过:", result.data.name);
return result.data;
} else {
console.error("校验失败:", result.error.issues);
return null;
}
}
// 测试
const validData = { id: 1, name: "小明", email: "ming@example.com" };
const invalidData = { id: "1", name: "小明", email: "not-an-email" };
safeParseUser(validData); // 校验通过
safeParseUser(invalidData); // 校验失败,给出详细错误
步骤 6:zod + fetch 完整封装
import { z } from "zod";
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
});
const UserListSchema = z.array(UserSchema);
type User = z.infer<typeof UserSchema>;
async function fetchUsers(): Promise<User[]> {
const response = await fetch("https://api.example.com/users");
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const raw: unknown = await response.json(); // 先用 unknown 接住
return UserListSchema.parse(raw); // 运行时校验,不合格就抛错
}
// 调用方拿到的就是校验过的 User[]
async function main2() {
try {
const users = await fetchUsers();
for (const user of users) {
console.log(user.name); // 编译期和运行时都安全
}
} catch (error) {
if (error instanceof z.ZodError) {
console.error("数据格式不符合预期:", error.issues);
} else if (error instanceof Error) {
console.error("请求失败:", error.message);
}
}
}
步骤 7:错误处理的类型模式
// 模式一:返回 Result 类型(不抛异常)
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
async function safeFetchUser(id: number): Promise<Result<User>> {
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
return { ok: false, error: new Error(`HTTP ${response.status}`) };
}
const data = UserSchema.parse(await response.json());
return { ok: true, value: data };
} catch (error) {
return {
ok: false,
error: error instanceof Error ? error : new Error("未知错误"),
};
}
}
// 调用方被迫处理错误(编译器强制)
async function demo2() {
const result = await safeFetchUser(1);
if (result.ok) {
console.log(result.value.name); // value 存在
} else {
console.error(result.error.message); // error 存在
}
}
步骤 8:并发请求的类型
async function fetchDashboard(): Promise<{
user: User;
posts: Post[];
stats: Stats;
}> {
// Promise.all 保持类型元组
const [user, posts, stats] = await Promise.all([
fetchUser(1),
fetchPosts(),
fetchStats(),
]);
return { user, posts, stats };
}
// 如果某个请求可能失败,用 allSettled
async function fetchDashboardSafe(): Promise<{
user: User | null;
posts: Post[] | null;
stats: Stats | null;
}> {
const [userResult, postsResult, statsResult] = await Promise.allSettled([
fetchUser(1),
fetchPosts(),
fetchStats(),
]);
return {
user: userResult.status === "fulfilled" ? userResult.value : null,
posts: postsResult.status === "fulfilled" ? postsResult.value : null,
stats: statsResult.status === "fulfilled" ? statsResult.value : null,
};
}
讲清楚要点
Promise<T>的泛型参数是 resolve 值的类型:Promise<void>表示没有返回值(如delay),Promise<User>表示 resolve 一个User。async函数的返回类型自动包装:async function f(): Promise<string>里return "hello"是合法的,TS 自动包装成Promise<string>。response.json()返回any:这是 TS 和 DOM 类型的历史包袱。必须手动处理:类型断言as T、或 zod 运行时校验。- zod 是「类型 + 运行时校验」二合一:
z.infer<typeof Schema>生成类型,.parse()做运行时校验。一份代码,两种保障。 catch里的error默认是unknown:TS 4.4+ 的严格行为。必须先instanceof Error或自定义守卫收窄,不能直接.message。- Result 模式比异常更安全:
{ ok: true, value } | { ok: false, error }让调用方被迫处理错误分支,异常可以被 try/catch 漏掉。
你可能会懵的地方
-
Promise<T>和Promise<Promise<T>>会嵌套吗? 不会。Promise会自动展平:Promise.resolve(Promise.resolve(1))的类型是Promise<number>,不是Promise<Promise<number>>。 -
async 函数里抛异常,返回类型是什么? 仍然是
Promise<T>。异常会让 Promise reject,调用方用.catch()或try/catch捕获。类型签名上看不出是否会抛异常。 -
zod 的
parse和safeParse怎么选?parse失败抛异常,适合「不合格就不该继续」的场景;safeParse返回结果对象,适合需要优雅降级的场景(比如表单校验)。 -
as Promise<T>断言安全吗? 不安全。它只是告诉编译器「相信我」。如果运行时数据不符,断言不会发现。生产环境必须用 zod 等运行时校验。 -
为什么
unknown比any更适合接 API 返回?any可以任意操作不报错;unknown必须先校验/收窄才能用。API 返回先用unknown接住,逼你做运行时校验,避免「类型污染」扩散到整个代码库。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
fetch(...).then(r => r.json()) 直接当 User 用 | json() 返回 any,无类型安全 | 用 zod 校验或 as User + 信任后端 |
catch (e) { console.log(e.message) } | e 是 unknown,.message 报错 | if (e instanceof Error) 收窄 |
| 以为 TS 类型能校验 API 返回 | 类型编译后消失,运行时无保障 | 编译期 TS + 运行时 zod 双保险 |
async function f(): Promise<void> 里 return data | void 不允许返回有用值 | 改返回类型为 Promise<T> |
多个 await 串行执行 | 慢 | 无依赖关系的用 Promise.all 并发 |
卡点急救
Property 'xxx' does not exist on type 'unknown':API 返回标成了unknown,先用 zod 或类型守卫收窄。Cannot find name 'fetch':Node 环境没有 fetch 类型。装@types/node或升级 Node 18+(内置 fetch)。- zod 报错
Expected string, received number:后端返回类型和预期不符。检查后端文档,或放宽 schema(z.union([z.string(), z.number()]))。 Promise.all的类型推不出来:确保每个 Promise 的类型不同,用as const或显式元组类型标注。
面试常问
-
TS 的
Promise类型是怎么工作的?Promise<T>是泛型接口,泛型参数T表示resolve出来的值的类型。async函数自动把返回值包装成Promise<T>。await把Promise<T>解包成T。 -
如何给 API 响应建模? 用判别联合:成功响应一个接口(含
data),失败响应一个接口(含error),用code或status字段做判别。配合类型守卫收窄。 -
TS 类型能保证 API 数据正确吗? 不能。类型只在编译期存在,运行时被擦除。后端返回错误数据时 TS 无法发现。需要 zod、io-ts 等运行时校验库。
-
catch里的 error 为什么是unknown? JS 允许 throw 任何值(throw "error"、throw { code: 500 })。TS 4.4+ 严格模式下catch参数默认unknown,强制你收窄后再使用。
自测三问
async function f(): Promise<string>里return 42会报错吗?为什么?- 为什么
response.json()返回any是危险的?如何修复? - zod 的
z.infer解决了什么问题?相比手写 interface 有什么优势?
小结一句话
异步 API 的类型安全要双管齐下:编译期用 Promise<T> 和判别联合建模,运行时用 zod 校验数据形状——类型系统管代码,运行时校验管数据,缺一不可。
延伸
- zod 官方文档
- Handbook: More on Functions - async/await
- 下一课:综合练习与 Next.js——把 12 课所有知识点串成一个完整项目。