异步与 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 运行时校验
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 校验数据形状——类型系统管代码,运行时校验管数据,缺一不可。
延伸