大多数 TypeScript 项目对异步类型的处理停留在 Promise<T> 的层面——知道 async 函数返回 Promise<T>,知道 await 会展开它。但实际项目里遇到的异步类型问题远比这个复杂:并发控制函数的类型签名怎么写?错误怎么建模?多个不同来源的异步结果怎么合并?
Promise 的类型展开规则
Promise<T> 是 TypeScript 中少数几个有"编译器感知"的泛型类型。await 表达式会自动展开一层 Promise:
async function example() {
const p: Promise<Promise<string>> = Promise.resolve(Promise.resolve('hello'))
const r = await p
// r 的类型是 Promise<string>,而不是 string
// await 只展开一层
}
await 只展开最外层 Promise。如果你需要递归展开,需要自己做类型体操:
type AwaitedDeep<T> = T extends Promise<infer U> ? AwaitedDeep<U> : T
type Deep = AwaitedDeep<Promise<Promise<Promise<string>>>>
// string
TypeScript 4.5 内置了 Awaited<T> 类型,但它只展开到 Promise 不再嵌套为止——和 await 的行为一致,不是递归展开所有包装器。
并发控制的类型签名
Promise.all 的类型处理
Promise.all 接受一个可迭代对象。TypeScript 对元组的处理比较精确:
const results = await Promise.all([
fetchUser(), // Promise<UserResponse>
fetchPosts(), // Promise<Post[]>
fetchComments(), // Promise<Comment[]>
])
// results: [UserResponse, Post[], Comment[]]
但如果传入的是数组而不是元组:
const promises: Promise<unknown>[] = [fetchUser(), fetchPosts()]
const results = await Promise.all(promises)
// results: unknown[] —— 信息丢失了
这是因为 TypeScript 无法确定动态数组中每个元素的类型。解决方法是显式标注元组类型:
const promises = [fetchUser(), fetchPosts()] as const
// 或者用 satisfies
const promises = [fetchUser(), fetchPosts()] satisfies [Promise<UserResponse>, Promise<Post[]>]
封装并发控制函数
自定义并发控制函数的类型签名需要保留每个 Promise 的类型:
async function parallelAll<T extends readonly Promise<any>[]>(
promises: T
): Promise<{ -readonly [K in keyof T]: Awaited<T[K]> }>
async function parallelAll<T extends readonly Promise<any>[]>(promises: T) {
return Promise.all(promises) as Promise<{ -readonly [K in keyof T]: Awaited<T[K]> }>
}
// 使用示例
const [user, posts] = await parallelAll([
fetchUser(),
fetchPosts(),
] as const)
// user: UserResponse, posts: Post[]
这里的 as const 是必须的——否则 ['a', 'b'] 会被推断为 string[] 而不是元组。
Result 模式的类型实现
JavaScript 的错误处理依赖 try/catch,但 catch 里的错误类型是 unknown。这不是 TS 的限制,而是 JavaScript 的设计——任何值都可以 throw。Result 模式把成功和失败编码到返回值里,避免隐式的异常路径。
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E }
function wrap<T>(fn: () => T): Result<T> {
try {
return { ok: true, value: fn() }
} catch (e) {
return { ok: false, error: e instanceof Error ? e : new Error(String(e)) }
}
}
async function wrapAsync<T>(promise: Promise<T>): Promise<Result<T>> {
try {
const value = await promise
return { ok: true, value }
} catch (e) {
return { ok: false, error: e instanceof Error ? e : new Error(String(e)) }
}
}
使用 Result 模式后,调用方必须显式检查 ok 字段:
const result = await wrapAsync(fetchUser())
if (result.ok) {
// result.value: UserResponse
console.log(result.value.name)
} else {
// result.error: Error
console.error(result.error.message)
}
可辨识联合在这里起作用——检查 result.ok 之后,success 和 error 分支被严格隔离。如果不检查,编译器会强制你处理两种情况。
带错误码的 Result
type ApiError = {
code: 'NOT_FOUND' | 'UNAUTHORIZED' | 'RATE_LIMITED' | 'SERVER_ERROR'
message: string
retryAfter?: number
}
type ApiResult<T> = Result<T, ApiError>
function handleApiResult<T>(result: ApiResult<T>) {
if (!result.ok) {
switch (result.error.code) {
case 'NOT_FOUND':
return showNotFound()
case 'UNAUTHORIZED':
return redirectToLogin()
case 'RATE_LIMITED':
return scheduleRetry(result.error.retryAfter ?? 5000)
case 'SERVER_ERROR':
return showServerError()
}
}
return renderData(result.value)
}
这里的 ApiError 不是简单字符串——每个错误码携带不同的额外信息。RATE_LIMITED 有 retryAfter,其他错误码可能没有。
多个异步操作的类型合并
当多个异步操作的结果类型需要合并时,经常用到 mapped types:
type AsyncMap<T extends Record<string, any>> = {
[K in keyof T]: Promise<T[K]>
}
async function resolveAll<T extends Record<string, any>>(
obj: AsyncMap<T>
): Promise<T> {
const entries = Object.entries(obj)
const resolved = await Promise.all(entries.map(([_, v]) => v))
return Object.fromEntries(
entries.map(([k], i) => [k, resolved[i]])
) as T
}
// 使用
const result = await resolveAll({
user: fetchUser(),
posts: fetchPosts(),
meta: fetchMeta(),
})
// result: { user: UserResponse; posts: Post[]; meta: MetaData }
这个模式对比手动写多个 const x = await ... 的好处是:所有请求并发执行,且结果类型保持精确。
异步错误处理中的类型分级
不是所有错误都需要同样的处理策略。用联合类型可以建模不同级别的错误:
type AppError =
| { level: 'info'; message: string } // 可忽略
| { level: 'warning'; message: string; retryFn: () => Promise<void> }
| { level: 'critical'; message: string; fatal: true }
type AppResult<T> = Promise<Result<T, AppError>>
async function fetchData(): AppResult<Data> {
try {
const data = await api.get('/data')
return { ok: true, value: data }
} catch (e) {
if (e instanceof RateLimitError) {
return {
ok: false,
error: { level: 'warning', message: '限流,稍后重试', retryFn: () => fetchData() }
}
}
return {
ok: false,
error: { level: 'critical', message: '服务不可用', fatal: true }
}
}
}
总结
| 场景 | 类型手段 | 注意点 |
|---|---|---|
| 单层 Promise 展开 | Awaited<T> 或 await | 只展开一层 |
| 多层嵌套展开 | 自定义递归 AwaitedDeep | 深度限制 |
| 并发结果保留类型 | 元组 + mapped types | 配合 as const |
| 错误建模 | 可辨识联合 + Result 模式 | 穷举检查 error 类型 |
| 异步 map 合并 | mapped types | 并发 + 类型保留 |
异步编程的类型安全核心不是 Promise 本身,而是如何把成功和失败编码到类型系统里让它不能忽略。Result 模式比 try/catch 更安全,因为编译器会强迫你检查——catch 里的错误类型永远是 unknown,而 Result 可以把错误建模成精确的联合类型。


