第 15 章

async/Promise 类型:Promise.all 推导、async 错误类型

第15章:async/Promise 类型:Promise.all 推导、async 错误类型

理解async/Promise 类型是掌握 TypeScript 类型系统的关键一步。

本章核心问题:如何在实际项目中正确使用Promise.all 推导、async 错误类型?关键的设计决策和陷阱是什么?

读完本章你将理解


Level 1 · 你需要知道的(1-3年经验)

Promise<T> 类型基础

async 函数的返回类型总是 Promise<T>,其中 T 是函数体最终 return 值的类型。TypeScript 能自动推导这个 T,但理解推导规则能避免意外。

// TypeScript 推导 async 函数的返回类型
async function getUsername(id: string): Promise<string> {
  const user = await db.users.findById(id);
  return user.name; // string → Promise<string>
}

// 不写返回类型注解,TS 也能正确推导
async function getCount() {
  return 42; // 推导为 Promise<number>
}

// 但这个陷阱要小心:
async function risky() {
  if (Math.random() > 0.5) return "string";
  return 42;
}
// 推导为 Promise<string | number> — 可能不是你想要的
// 最好明确标注:async function risky(): Promise<string | number>

Awaited<T>:展开嵌套 Promise

TypeScript 4.5 引入 Awaited<T>,它递归展开 Promise 包装,在处理泛型异步函数时非常重要。

type A = Awaited<Promise<string>>;           // string
type B = Awaited<Promise<Promise<number>>>;  // number(递归展开)
type C = Awaited<string>;                    // string(非 Promise 原样返回)

// 实际应用场景:获取 async 函数的返回值类型
async function fetchUser(): Promise<User> { ... }

type FetchResult = Awaited<ReturnType<typeof fetchUser>>; // User,不是 Promise<User>

// 泛型工具函数
async function withTimeout<T>(
  promise: Promise<T>,
  ms: number
): Promise<T> {
  const timeout = new Promise<never>((_, reject) =>
    setTimeout(() => reject(new Error(`Timeout after ${ms}ms`)), ms)
  );
  return Promise.race([promise, timeout]);
}

// T 被正确推导
const user = await withTimeout(fetchUser(), 5000); // user: User,不是 User | never

Promise.all:元组推导 vs 数组推导

Promise.all 是 TypeScript 类型推导的展示台——传入元组时推导出元组,传入数组时推导出数组。

// 元组:每个位置有精确类型
const [user, posts, comments] = await Promise.all([
  getUser(userId),     // Promise<User>
  getPosts(userId),    // Promise<Post[]>
  getComments(userId), // Promise<Comment[]>
]);
// user: User
// posts: Post[]
// comments: Comment[]

// 这是可能的,因为 Promise.all 有元组重载:
// all<T extends readonly unknown[]>(values: [...{ [K in keyof T]: PromiseLike<T[K]> }]): Promise<T>
// 数组:所有元素类型取联合
const promises: Promise<string>[] = [fetch1(), fetch2(), fetch3()];
const results = await Promise.all(promises);
// results: string[] — 均匀类型数组

// 混合类型数组的陷阱
const mixed = [getUser(id), getPosts(id)]; // 推导为 (Promise<User> | Promise<Post[]>)[]
const result = await Promise.all(mixed);
// result: (User | Post[])[] — 失去了精确位置类型!

// 修复:用 as const 保持元组类型
const fixed = [getUser(id), getPosts(id)] as const;
const [u, p] = await Promise.all(fixed);
// u: User, p: Post[] — 正确

Promise.allSettled 和 PromiseSettledResult<T>

allSettled 等待所有 Promise 完成(无论成功还是失败),返回每个的状态。

type PromiseSettledResult<T> =
  | { status: "fulfilled"; value: T }
  | { status: "rejected";  reason: unknown };

// 实际用法:并行请求,部分失败不影响其余
const results = await Promise.allSettled([
  fetchUserProfile(id),   // Promise<UserProfile>
  fetchUserPosts(id),     // Promise<Post[]>
  fetchUserFollowers(id), // Promise<User[]>
]);

// results: PromiseSettledResult<UserProfile | Post[] | User[]>[]
// 注意:因为输入是数组(非元组),类型取联合

// 用元组保持精确类型
const [profileResult, postsResult, followersResult] = await Promise.allSettled([
  fetchUserProfile(id),
  fetchUserPosts(id),
  fetchUserFollowers(id),
] as const);

// profileResult:  PromiseSettledResult<UserProfile>
// postsResult:    PromiseSettledResult<Post[]>
// followersResult: PromiseSettledResult<User[]>

// 筛选成功的结果
const fulfilled = results.filter(
  (r): r is PromiseFulfilledResult<UserProfile | Post[] | User[]> =>
    r.status === "fulfilled"
);

Level 2 · 它是怎么运行的(3-5年经验)

Promise.race 和 Promise.any

// Promise.race:第一个完成(无论成功失败)的结果
async function fetchWithFallback(
  primary: Promise<User>,
  fallback: Promise<User>
): Promise<User> {
  return Promise.race([primary, fallback]); // Promise<User>
}

// race 的类型:所有输入类型的联合
const raceResult = await Promise.race([
  getUser(id),   // Promise<User>
  getAdmin(id),  // Promise<Admin>
]);
// raceResult: User | Admin

// Promise.any:第一个成功的结果(ES2021)
// 如果所有都失败,抛出 AggregateError
async function tryMultipleSources(id: string): Promise<User> {
  return Promise.any([
    primaryDb.getUser(id),
    replicaDb.getUser(id),
    cacheDb.getUser(id),
  ]);
}

异步生成器:AsyncGenerator<T, TReturn, TNext>

异步生成器同时处理异步和迭代,类型签名包含三个参数。

// AsyncGenerator<T, TReturn, TNext>
// T       — yield 的值类型
// TReturn — return 的值类型(默认 void)
// TNext   — next() 传入值的类型(默认 unknown)

async function* paginate<T>(
  fetchPage: (cursor: string | null) => Promise<{ data: T[]; nextCursor: string | null }>
): AsyncGenerator<T, void, unknown> {
  let cursor: string | null = null;

  while (true) {
    const { data, nextCursor } = await fetchPage(cursor);
    for (const item of data) {
      yield item; // yield 类型是 T
    }
    if (!nextCursor) break;
    cursor = nextCursor;
  }
}

// 使用:for-await-of 自动推导类型
async function processAllPosts(userId: string) {
  const generator = paginate<Post>(cursor =>
    fetch(`/api/posts?user=${userId}&cursor=${cursor ?? ""}`).then(r => r.json())
  );

  for await (const post of generator) {
    // post: Post — 正确推导
    await processPost(post);
  }
}

async 错误类型问题

catch 永远是 unknown——这是 TypeScript 的有意设计,因为任何值都可以被 throw。

// catch 类型问题
async function riskyOperation(): Promise<void> {
  throw "string error";    // 任何类型都能被 throw
  // throw 42;
  // throw { code: 404 };
  // throw new Error("standard");
}

async function main() {
  try {
    await riskyOperation();
  } catch (e) {
    // e: unknown — TypeScript 是对的,你不知道抛了什么
    e.message; // 编译错误:Object is of type 'unknown'

    // 必须收窄类型
    if (e instanceof Error) {
      console.error(e.message); // string
    } else if (typeof e === "string") {
      console.error(e);
    }
  }
}

模式一:类型化错误包装器

为你自己的错误创建类层次,然后用 instanceof 收窄。

// 定义错误类层次
class AppError extends Error {
  constructor(
    message: string,
    public readonly code: string
  ) {
    super(message);
    this.name = "AppError";
  }
}

class NetworkError extends AppError {
  constructor(
    message: string,
    public readonly statusCode: number
  ) {
    super(message, "NETWORK_ERROR");
    this.name = "NetworkError";
  }
}

class NotFoundError extends AppError {
  constructor(resource: string, id: string) {
    super(`${resource} not found: ${id}`, "NOT_FOUND");
    this.name = "NotFoundError";
  }
}

class ValidationError extends AppError {
  constructor(
    message: string,
    public readonly field: string
  ) {
    super(message, "VALIDATION_ERROR");
    this.name = "ValidationError";
  }
}

// 使用:catch 后用 instanceof 精确分发
async function handleRequest(id: string) {
  try {
    const user = await fetchUser(id);
    return user;
  } catch (e) {
    if (e instanceof NetworkError) {
      if (e.statusCode === 401) return redirectToLogin();
      if (e.statusCode === 404) return show404();
      throw e; // 其他网络错误向上冒泡
    }
    if (e instanceof ValidationError) {
      return showFieldError(e.field, e.message);
    }
    throw e; // 未知错误:向上冒泡
  }
}

模式二:async + Result<T, E> 结合

把第 14 章的 Result 模式与 async 结合,彻底消除隐藏的异常。

type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E };
const ok  = <T>(value: T): Result<T, never>  => ({ ok: true,  value });
const err = <E>(error: E): Result<never, E>  => ({ ok: false, error });

// 真实场景:并行 API 请求 + 类型化错误处理
type ApiError =
  | { code: "NETWORK";     status: number }
  | { code: "NOT_FOUND";   resource: string }
  | { code: "UNAUTHORIZED" }
  | { code: "RATE_LIMITED"; retryAfter: number };

async function safeGet<T>(url: string): Promise<Result<T, ApiError>> {
  try {
    const res = await fetch(url);

    if (res.status === 401) return err({ code: "UNAUTHORIZED" });
    if (res.status === 404) return err({ code: "NOT_FOUND", resource: url });
    if (res.status === 429) {
      const retryAfter = Number(res.headers.get("Retry-After") ?? 60);
      return err({ code: "RATE_LIMITED", retryAfter });
    }
    if (!res.ok) return err({ code: "NETWORK", status: res.status });

    return ok(await res.json() as T);
  } catch (e) {
    return err({ code: "NETWORK", status: 0 }); // 网络完全失败
  }
}

// 并行请求,精确错误处理
async function loadDashboard(userId: string) {
  const [userResult, postsResult, analyticsResult] = await Promise.all([
    safeGet<User>(`/api/users/${userId}`),
    safeGet<Post[]>(`/api/posts?author=${userId}`),
    safeGet<Analytics>(`/api/analytics/${userId}`),
  ]);

  if (!userResult.ok) {
    // userResult.error: ApiError — 完整类型
    if (userResult.error.code === "UNAUTHORIZED") return redirectToLogin();
    throw new Error(`Failed to load user: ${userResult.error.code}`);
  }

  const posts = postsResult.ok ? postsResult.value : []; // 帖子可降级
  const analytics = analyticsResult.ok ? analyticsResult.value : null;

  return {
    user: userResult.value, // User — 类型安全
    posts,                   // Post[]
    analytics,               // Analytics | null
  };
}

真实案例:带超时和重试的并行请求

type FetchConfig = {
  timeoutMs: number;
  retries: number;
  retryDelayMs: number;
};

async function fetchWithRetry<T>(
  url: string,
  config: FetchConfig
): Promise<Result<T, ApiError>> {
  for (let attempt = 0; attempt <= config.retries; attempt++) {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), config.timeoutMs);

    try {
      const res = await fetch(url, { signal: controller.signal });
      clearTimeout(timeoutId);

      if (res.ok) return ok(await res.json() as T);
      if (res.status === 429) {
        const retryAfter = Number(res.headers.get("Retry-After") ?? config.retryDelayMs / 1000);
        if (attempt < config.retries) {
          await delay(retryAfter * 1000);
          continue;
        }
        return err({ code: "RATE_LIMITED", retryAfter });
      }
      return err({ code: "NETWORK", status: res.status });
    } catch (e) {
      clearTimeout(timeoutId);
      if (attempt < config.retries) {
        await delay(config.retryDelayMs * Math.pow(2, attempt)); // 指数退避
        continue;
      }
      return err({ code: "NETWORK", status: 0 });
    }
  }
  return err({ code: "NETWORK", status: 0 });
}

function delay(ms: number): Promise<void> {
  return new Promise(resolve => setTimeout(resolve, ms));
}

// 使用
const userResult = await fetchWithRetry<User>("/api/user/123", {
  timeoutMs: 5000,
  retries: 3,
  retryDelayMs: 1000,
});

Level 3 · 规范怎么定义的(资深)

TypeScript 对 Promise.all 的类型推导使用了元组映射(tuple mapping):当传入的参数是元组类型时,返回类型也是元组,每个位置对应 Awaited<T[K]>Awaited<T>(TS 4.5)不直接匹配 Promise<T>,而是匹配 .then 方法签名中的 onfulfilled 回调参数——这使它能处理任何 thenable 对象。AsyncGenerator<T, TReturn, TNext> 的三个类型参数分别对应 yield 值、return 值和 next() 传入值。

Level 4 · 边界与陷阱(所有人)

反模式:async () => any 丢失所有类型

// 反例:返回 any 感染整个调用链
const fetchData = async (): Promise<any> => {
  return fetch("/api/data").then(r => r.json());
};

const data = await fetchData(); // any
data.user.profile.avatar;       // 不报错,运行时可能是 undefined
// 正解:明确返回类型,或让推导工作
interface ApiResponse {
  user: User;
  posts: Post[];
}

const fetchData = async (): Promise<ApiResponse> => {
  const raw = await fetch("/api/data").then(r => r.json());
  return ApiResponseSchema.parse(raw); // 验证 + 类型收窄
};

const data = await fetchData();  // ApiResponse
data.user.profile.avatar;        // 编译器检查每一层属性访问

汇总

特性 TypeScript 版本 关键点
Promise<T> 基础 2.1+ async 函数自动包装返回值
Awaited<T> 4.5+ 递归展开 Promise,泛型必备
Promise.all 元组推导 3.9+ 输入需为元组或用 as const
Promise.allSettled 4.1+ 返回 PromiseSettledResult<T>[]
Promise.any 4.4+ 返回第一个成功,失败抛 AggregateError
AsyncGenerator<T,R,N> 3.6+ 三个类型参数各有含义
catch 类型 4.0+ 总是 unknown,需手动收窄

下一章预告

第 16 章进入条件类型与类型推断infer 关键字、Conditional Types 的分布特性、以及如何用类型级编程实现 ReturnType<F>Parameters<F> 等内置工具的底层原理。

本章评分
4.8  / 5  (18 评分)

💬 留言讨论