在 JavaScript 项目逐步引入 TypeScript 后,异步代码往往最容易出现“看起来没问题,类型却越来越松”的情况。要把 Promise 和 async/await 写得清楚,重点不在于给每一行都加类型,而是明确三个判断标准:async 函数最终返回什么、await 拿到的到底是什么、失败路径是否也被类型系统覆盖。下面按这几个问题展开,梳理一套更适合日常项目的写法。
async 函数返回值为什么要显式写成 Promise<T>
在 TypeScript 中,async 函数会自动把返回值包装成 Promise。因此,函数签名里真正应该描述的,不是同步返回的对象,而是“异步完成后 resolve 出来的值”。最直接也最稳定的写法,就是显式标注为 Promise<T>。

这样做有三个实际好处:一是阅读函数签名时就能知道它一定是异步函数;二是复杂分支里不容易出现返回类型被意外放宽的问题;三是编辑器在调用处能给出更准确的提示。
async function getUser(): Promise<{ id: number; name: string }> {
return { id: 1, name: 'Alice' };
}
下面几种情况需要区分清楚:
- ✅ 正确:
async function getUser(): Promise<{ id: number; name: string }> { return { id: 1, name: 'Alice' }; } - ❌ 错误:
async function getUser(): { id: number; name: string } { ... },TypeScript 会报错:Type '{ id: number; name: string; }' is not assignable to type 'Promise<{ id: number; name: string; }>' - ⚠️ 可省略但不推荐:
async function getUser() { return { id: 1, name: 'Alice' }; },这时会推导为Promise<{ id: number; name: string }>,但逻辑复杂时推导结果未必始终符合预期
简化理解就是:async 函数签名里写的 T,永远应该是最终业务值的类型,而函数声明本身写成 Promise<T>。
await 拿到的是什么类型
await 的作用不是“得到一个 Promise”,而是把 Promise 解包,拿到里面最终 resolve 的值。所以在类型层面,await 表达式的结果类型就是 Promise<T> 里的 T。
这一点在接口请求场景里最常见:
const res = await fetch('/api/user');,这里的res类型是Response,不是Promise<Response>const data = await res.json();,如果res.json()返回Promise<User>,那么data的类型就是User
对应代码可以这样理解:
const res = await fetch('/api/user');
const data = await res.json();
如果当前上下文里返回值类型不够明确,可以补上类型断言或显式说明:
const data = await res.json() as User;
// 或
const data = await (res.json() as Promise<User>);
这里需要注意的是,类型清晰的关键不在于“多写断言”,而在于尽量让返回 Promise 的函数本身就给出可靠类型。调用端频繁断言,往往说明上游函数签名还不够准确。
异步错误处理怎么避免类型失控
相比成功分支,很多 TypeScript 异步代码真正失守的地方在错误处理。原生 TypeScript 不会严格检查 Promise 被 reject 时抛出的具体类型,因此如果错误分支不加约束,类型系统很容易退化。
实践里更可用的做法主要有两种。
在 catch 中把错误当成 unknown 处理
比起直接写一个宽泛的 catch (err),更稳妥的方式是把它明确成 unknown,再通过类型守卫缩小范围:

catch (err: unknown) {
if (err instanceof Error) {
console.error(err.message);
}
}
这样做的意义很直接:你不会默认假设所有抛出的值都有 message 属性,必须先判断再使用。
给可能失败的异步结果设计联合类型
如果某个异步函数天然有成功和失败两种业务结果,比起把所有信息都丢进异常,更适合返回一个带状态的结果类型,例如 Promise<Result<SuccessData, ApiError>>。这样调用端不只知道“会失败”,还知道失败时会拿到什么结构的数据。
原文提到的 Promise<T, E> 形式并不是原生 TypeScript 的 Promise 语法,因此更适合作为概念说明,而不是直接照搬到项目代码里。落到工程实践,联合类型、自定义 Result 类型,以及配合类型守卫,通常更实用。
还有一点需要明确:如果写成 catch (err) 且不声明类型,err 很容易滑向 any,这会直接失去类型保护。
Promise 构造和静态方法,类型必须和实际值对齐
当你手动创建 Promise,或者使用 Promise.all 这类静态方法时,类型的核心原则仍然没变:泛型声明必须和实际返回值一致。
手动 new Promise 时,resolve 的值要匹配泛型
new Promise<string>((resolve) => resolve('ok'))
new Promise<number>((resolve) => resolve('ok'))
上面两行里,第一种是正确的,因为声明的是 string,实际传入的也是字符串 'ok';第二种则会发生类型不匹配,因为泛型写的是 number,但 resolve 的实际值是字符串。
Promise.all 会按输入 Promise 自动推导结果
在并发请求里,Promise.all 的推导能力非常实用:
const [a, b] = await Promise.all([fetch('/a'), fetch('/b')]);
这里 a 和 b 的类型都会是 Response。如果数组里每个 Promise 的结果类型不同,就可以用元组把顺序和类型一起固定下来:
Promise.all<[User, Order[]]>([getUser(), getOrders()])
这种写法尤其适合“一个用户对象 + 一组列表数据”之类的组合请求。它的价值在于,解构后的每个变量都会保留准确类型,而不会被合并成模糊的联合结果。
日常项目里可以直接落地的几条规则
把前面的要点压缩成工程规则,基本可以归纳为下面几条:
async函数尽量显式写返回类型,并始终写成Promise<T>await后拿到的是解包后的T,不要把它继续当作 Promise 使用- 错误分支优先使用
unknown+ 类型守卫,或返回明确的联合结果类型 - 手动创建 Promise 时,泛型参数要和
resolve的实际值保持一致 - 使用
Promise.all处理异构结果时,优先考虑元组标注,避免类型信息丢失
如果用一句话总结,TypeScript 规范异步返回类型的核心就是:让函数签名描述“最终得到什么”,而不是只描述“它是不是 Promise”。只要这一点保持清楚,Promise、await 和错误处理之间的类型关系就会稳定很多。













