Menu

TypeScript Promise:Promise<T>、Promise.all 与 allSettled

TypeScript 中如何为 Promise 标注类型:Promise<T> 类型、为 new Promise 和 resolve 标注类型、then 如何改变类型、为什么 catch 给你的是 any、返回元组结果的 Promise.all、Promise.allSettled 的结果类型,以及把回调式 API 包装成带类型的 Promise。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

在 TypeScript 中,Promise 的类型是 Promise<T>,其中 T 是它兑现时的值的类型。返回 Promise 的函数在返回类型中声明它,而你通过 then 或 await 得到的值类型就是 T。

Promise 的运行机制(状态、微任务队列、链式调用)属于普通 JavaScript,在 JavaScript Promise 中讲解。本页只讲类型。

为 new Promise 和 resolve 标注类型

当 new Promise 作为一个声明了返回类型的函数的返回值时,T 就来自那个返回类型。单独使用时,TypeScript 不会从 resolve 调用中推断 T,你得到的是 Promise<unknown>。显式传入类型参数:

有两个细节。resolve 的类型来自 T,所以在 Promise<number> 中写 resolve("42") 会编译出错。而不带参数的 resolve() 只有在 T 包含 void 时才允许:在 Promise<number> 中它会报错 TS2794(Expected 1 arguments, but got 0. Did you forget to include 'void' in your type argument to 'Promise'?)。

reject 这一边没有类型。并不存在 Promise<T, E>:Promise 可以用任何值拒绝。

then、catch 与 finally

每个 then 都返回一个新的 Promise,其类型由回调的返回值决定。如果回调返回的是 Promise,TypeScript 会把它解开,所以你永远不会得到 Promise<Promise<T>>。

在标准库中,catch 回调的参数类型是 any,而不是 unknown。没有什么能阻止你在一个可能是字符串的值上写 e.message。像上面那样把它标注为 unknown,使用前先收窄。在 await 外面套 try/catch 会更好:在 strict 下,它的 catch 变量已经是 unknown(参见错误处理)。

Promise.all 返回元组

传入由不同 Promise 类型组成的数组字面量时,Promise.all 返回一个元组的 Promise,每个位置都保留自己的类型:

可以混入非 Promise 的值,它们会原样传递。只要有一个输入被拒绝,Promise.all 就会立即拒绝,其他结果都会丢失。Promise.race 以第一个完成的结果兑现或拒绝,类型是各输入的联合类型;Promise.any 以第一个成功的结果兑现,只有在所有输入都失败时才以 AggregateError 拒绝。

Promise.allSettled 及其结果类型

Promise.allSettled 会等待每一个输入,并且永远不会拒绝。每个结果都是 PromiseSettledResult<T>,这是一个联合类型,可以用 status 字段来收窄:

只有在检查 status 之后才能访问 r.value,因为拒绝的那个变体没有 value。reason 是 any,原因和 catch 中一样。filter 使用了类型谓词,这样过滤后的数组类型就是已兑现的结果。

把回调式 API 包装成 Promise

较老的 API 通过回调报告结果,通常是 Node 风格的 (err, result) => void。把它们包装一次,变成返回带类型 Promise 的函数,其余代码就可以使用 await 了:

在 Node 中,util.promisify 可以对遵循 (err, result) 约定的函数完成这件事,而且很多内置模块已经有了 Promise 版本(node:fs/promises、node:timers/promises)。

常见错误

  • 签名写的是 Promise<T> 却返回了 T,或者反过来。 一个声明为 Promise<User> 的非 async 函数必须返回 Promise;async 函数会自动返回 Promise。
  • 忘记处理 Promise。 像 save(user); 这样没有 await、then 或 catch 的调用可以正常编译,而一旦被拒绝,就会变成未处理的拒绝(默认会让 Node 进程退出)。typescript-eslint 的 no-floating-promises 规则可以捕获这类问题。
  • 相信 .catch((e) => ...) 的类型。 它的 e 是 any。把它标注为 unknown。
  • 给已经返回 Promise 的东西再套一层 new Promise。 直接返回或 await 现有的 Promise 就行。

常见问题

TypeScript 中的 Promise<T> 是什么?

Promise<T> 是一个以 T 类型的值兑现的 Promise 的类型。返回 Promise<string> 的函数交给你一个 Promise,它的 then 回调或 await 得到的是 string。不带值兑现的 Promise 是 Promise<void>。

TypeScript 中如何为 new Promise 标注类型?

传入类型参数:new Promise<number>((resolve, reject) => ...)。不传的话,TypeScript 无法从 resolve 调用中推断出值的类型,结果是 Promise<unknown>。如果 Promise 不带任何值兑现,就用 new Promise<void>(resolve => ...),这样才允许不带参数调用 resolve()。

Promise 的 catch 中错误是什么类型?

any。在标准库中,catch 回调的 reason 参数类型是 any,因为任何东西都可能被抛出或拒绝。自己把它标注为 unknown(.catch((e: unknown) => ...)),使用前再用 instanceof Error 收窄。

TypeScript 中 Promise.all 的类型是怎样的?

对数组字面量调用 Promise.all,返回的是元组类型,每个输入对应一个元素,顺序不变:Promise.all([getUser(), getCount()]) 是 Promise<[User, number]>,所以解构后每个值都有自己的类型。对同一类型的数组 T[] 调用,返回 Promise<T[]>。

Promise.all 和 Promise.allSettled 有什么区别?

只要有一个输入被拒绝,Promise.all 就会立即拒绝。Promise.allSettled 总是会兑现,结果是 PromiseSettledResult<T> 对象组成的数组:{ status: "fulfilled", value } 或 { status: "rejected", reason }。检查 status 就能收窄每一个结果。

Coddy 编程语言插图

和 Coddy 一起学编程

开始