async/Promise 类型:Promise.all 推导、async 错误类型
第15章:async/Promise 类型:Promise.all 推导、async 错误类型
理解async/Promise 类型是掌握 TypeScript 类型系统的关键一步。
本章核心问题:如何在实际项目中正确使用Promise.all 推导、async 错误类型?关键的设计决策和陷阱是什么?
读完本章你将理解:
- Promise<T> 类型基础
- Awaited<T>:展开嵌套 Promise
- Promise.all:元组推导 vs 数组推导
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> 等内置工具的底层原理。