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 仍能看到新客户。

详见 Sequelize reload 答疑。

10. 练习

  1. 观察漏 await 的查询变量类型。
  2. 对 map(async…) 分别使用 await 数组与 Promise.all,比较结果。
  3. 从一个 async 函数派生完成后的结果类型。
  4. 运行两个 example 的变体,确认 catch 捕获差别。

下一章:Express / Sequelize 的类型边界。