09 Promise、await 与返回类型复用
对应项目:findCustomers()、reload、replaceActorFilms()。
1. Promise 不是查询结果本身
const customer = Customer.findByPk(customer_id);
if (customer === null) {
// 错误思路:customer 是 Promise,不是 Customer | null。
}
加 await 后才拿到结果:
const customer = await Customer.findByPk(customer_id);
if (customer === null) throw new Error("客户不存在");
这里 Customer/customer_id 来自 Service 上下文。await 不只是让 TS 类型变化,还让后续语句等到 Promise 完成。拒绝会在 await 处作为异常抛出。
2. async 总是返回 Promise
async function getName(): Promise<string> {
return "ANN";
}
函数体返回字符串,外层调用拿到 Promise<string>。不能把 async 返回标成 string。
async function notify(): Promise<void> {
console.log("完成");
}
Promise<void> 表示完成信号没有业务返回值,不代表该操作不会失败,也不代表调用者不用 await。
3. Promise<T[]> 与 Promise<T>[]
| 类型 | 含义 |
|---|---|
Promise<CustomerDto[]> |
一次异步操作完成后得到 DTO 数组 |
Promise<CustomerDto>[] |
数组里每项都是尚待完成的异步操作 |
async function loadName(id: number): Promise<string> {
return `customer-${id}`; // 用纯函数模拟异步读取
}
const tasks = [1, 2, 3].map((id) => loadName(id)); // Promise<string>[]
const names = await Promise.all(tasks); // string[]
这是结果形状区别,不是两种可互换的写法。
4. map(async …) 与 forEach(async …) 的坑
const names = [1, 2].map(async (id) => loadName(id));
names 是 Promise 数组,不是字符串数组。await names 也不会等待数组内部每个 Promise,应 await Promise.all(names)。
[1, 2].forEach(async (id) => {
await loadName(id);
});
forEach 不等待回调 Promise,外层可能提前结束,也可能遗漏拒绝处理。顺序执行用:
for (const id of [1, 2]) {
await loadName(id);
}
独立读取并发可用 Promise.all,但不能无限创建数据库/HTTP 任务;要按连接池和资源限制控制并发。
Promise.all 失败不自动取消其余操作。写入场景尤其要注意其他操作仍可能继续;参加同一事务的查询也通常受单连接限制,不能因为 Promise.all 就认为数据库并行执行。
5. ReturnType:取得函数返回的类型
function getSummary() {
return { customer_id: 1, first_name: "ANN" };
}
type Summary = ReturnType<typeof getSummary>;
typeof getSummary 是函数类型;ReturnType 从它拿到返回类型。ReturnType 不调用函数,不读取数据。
函数参数可以使用 Parameters:
function searchCustomer(name: string, active: boolean) {
return { name, active };
}
type SearchArguments = Parameters<typeof searchCustomer>;
// [name: string, active: boolean]
type FirstArgument = SearchArguments[0]; // string
Parameters 得到参数元组,适合包装函数、测试等复用。公共 HTTP 输入通常仍值得有稳定、明确的名称。
6. Awaited:取得异步完成后的值类型
async function getSummary() {
return { customer_id: 1, first_name: "ANN" };
}
type SummaryPromise = ReturnType<typeof getSummary>;
// Promise<{ customer_id: number; first_name: string }>
type Summary = Awaited<SummaryPromise>;
// { customer_id: number; first_name: string }
也可以组合成:
type Summary = Awaited<ReturnType<typeof getSummary>>;
Awaited 是类型层面的解包,运行时仍需要 await。不能把 Promise 用 as Summary 断言后当作结果使用。
7. 从当前 Service 派生类型,何时合适?
import type { findCustomers } from "../../services/customer.service";
type FindCustomersResult = Awaited<ReturnType<typeof findCustomers>>;
type FindCustomersInput = Parameters<typeof findCustomers>[0];
这里 import type 只为了读取函数类型,不执行 Service 或初始化数据库。
适合:紧贴实现的测试、适配器、局部包装。已有导出的 FindCustomersOptions、CustomerListItemDto 等稳定名称时,直接 import 这些类型通常更容易阅读。
不要所有 API 都反向依赖函数推断返回:实现加一个字段,就可能无意扩大外部契约。公共边界显式返回 DTO,内部辅助函数适度推断,是当前项目合适的平衡。
重载函数与泛型函数的 ReturnType/Parameters 可能采用最后签名或较宽类型,不一定像简单函数那样精确。遇到复杂情况先查看编辑器类型,不盲套工具类型。
8. return Promise 与 return await 的 catch 差别
async function example(): Promise<string> {
try {
return Promise.reject(new Error("失败"));
} catch (error: unknown) {
return "降级值";
}
}
外层 Promise 会拒绝,但这里的同步 try/catch 不会捕获未来才发生的拒绝。
需要在本函数处理异步异常时:
async function example(): Promise<string> {
try {
return await Promise.reject(new Error("失败"));
} catch (error: unknown) {
return "降级值";
}
}
所以不要笼统认为 return await 总是多余。是否需要取决于本层错误处理和 finally 生命周期等需求。
9. await 与 transaction 解决不同问题
当前创建客户后:
await customer.reload({ include, transaction });
await 保证关联加载完成再映射;transaction 保证查询参加同一事务并读取其未提交数据。类型检查不能单独证明漏 transaction 的 SQL 仍能看到新客户。
10. 练习
- 观察漏 await 的查询变量类型。
- 对 map(async…) 分别使用 await 数组与 Promise.all,比较结果。
- 从一个 async 函数派生完成后的结果类型。
- 运行两个 example 的变体,确认 catch 捕获差别。