CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
TypeScript 核心/课时/异步与 API 类型
RawBlame
课时目录12 节

TypeScript 核心

  1. 01TypeScript 是什么
  2. 02基本类型
  3. 03函数类型
  4. 04接口与类型别名
  5. 05联合与交叉类型
  6. 06类型收窄
  7. 07泛型
  8. 08工具类型
  9. 09模块与 tsconfig
  10. 10React 组件 typing
  11. 11异步与 API 类型
  12. 12综合练习与 Next.js

异步与 API 类型

Promise、fetch 响应类型、可选 zod 简介。

30 分钟

异步与 API 类型

这一课你将能做到

  1. 给 Promise、async/await 函数标注正确类型,处理 Promise<T> 与 Promise<void>
  2. 用判别联合建模 API 响应,写出类型安全的 fetch 封装
  3. 用 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,
  };
}

讲清楚要点

  1. Promise<T> 的泛型参数是 resolve 值的类型:Promise<void> 表示没有返回值(如 delay),Promise<User> 表示 resolve 一个 User。
  2. async 函数的返回类型自动包装:async function f(): Promise<string> 里 return "hello" 是合法的,TS 自动包装成 Promise<string>。
  3. response.json() 返回 any:这是 TS 和 DOM 类型的历史包袱。必须手动处理:类型断言 as T、或 zod 运行时校验。
  4. zod 是「类型 + 运行时校验」二合一:z.infer<typeof Schema> 生成类型,.parse() 做运行时校验。一份代码,两种保障。
  5. catch 里的 error 默认是 unknown:TS 4.4+ 的严格行为。必须先 instanceof Error 或自定义守卫收窄,不能直接 .message。
  6. 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 datavoid 不允许返回有用值改返回类型为 Promise<T>
多个 await 串行执行慢无依赖关系的用 Promise.all 并发

卡点急救

  1. Property 'xxx' does not exist on type 'unknown':API 返回标成了 unknown,先用 zod 或类型守卫收窄。
  2. Cannot find name 'fetch':Node 环境没有 fetch 类型。装 @types/node 或升级 Node 18+(内置 fetch)。
  3. zod 报错 Expected string, received number:后端返回类型和预期不符。检查后端文档,或放宽 schema(z.union([z.string(), z.number()]))。
  4. 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,强制你收窄后再使用。

自测三问

  1. async function f(): Promise<string> 里 return 42 会报错吗?为什么?
  2. 为什么 response.json() 返回 any 是危险的?如何修复?
  3. zod 的 z.infer 解决了什么问题?相比手写 interface 有什么优势?

小结一句话

异步 API 的类型安全要双管齐下:编译期用 Promise<T> 和判别联合建模,运行时用 zod 校验数据形状——类型系统管代码,运行时校验管数据,缺一不可。

延伸

  • zod 官方文档
  • Handbook: More on Functions - async/await
  • 下一课:综合练习与 Next.js——把 12 课所有知识点串成一个完整项目。
加载本节练习…
← 上一课React 组件 typing下一课 →综合练习与 Next.js