Agent X-Ray
RuntimeNotesAbout
Notes/代码工程/TypeScript 深度教程/第5章

第5章:集合、异步与错误边界

4 分钟 · 更新于 2026-09-01

第5章:集合、异步与错误边界

本章把类型放进真实控制流:数组、Map、Set、迭代器、Promise、async/await 与错误处理。重点是让“可能失败、可能为空、可能并发”成为显式契约。


一、数组方法与类型推断

ts
interface Order {
  id: string;
  amount: number;
  paid: boolean;
}

const orders: Order[] = [
  { id: "A", amount: 100, paid: true },
  { id: "B", amount: 80, paid: false }
];

const paidTotal = orders
  .filter(order => order.paid)
  .map(order => order.amount)
  .reduce((sum, amount) => sum + amount, 0);

回调上下文通常能推断参数类型。复杂变换可抽成命名函数,提高错误定位和复用性。

二、Map 与普通对象

ts
const priceByRoute = new Map<string, number>();
priceByRoute.set("HKG-NRT", 1200);
const price = priceByRoute.get("HKG-NRT"); // number | undefined

Map 的优势:

  • 键不限于字符串。
  • size、遍历和增删语义明确。
  • 不受对象原型键影响。

普通对象适合 JSON 数据、固定字段和简单记录;Record<K, V> 适合有限键集合。

三、Set、WeakMap 与 WeakSet

ts
const airports = new Set<string>(["HKG", "NRT", "HKG"]);

Set 表达唯一值集合。WeakMap 和 WeakSet 的键必须是对象,且不阻止垃圾回收,适合给对象附加缓存或元数据;它们不可枚举,不适合作为普通数据容器。

四、迭代协议与生成器

任何实现 Symbol.iterator 的对象都可被 for...of 遍历。

ts
function* range(start: number, end: number): Generator<number> {
  for (let value = start; value <= end; value += 1) {
    yield value;
  }
}

for (const value of range(1, 3)) {
  console.log(value);
}

生成器按需产生值,适合惰性序列和状态机,但不要把简单数组逻辑过度改写成生成器。

五、Promise 类型

ts
function loadUser(id: string): Promise<User> {
  return fetch(`/api/users/${id}`).then(async response => {
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    return await response.json() as User;
  });
}

这里的 as User 仍没有运行时验证。更可靠的边界应先解析为 unknown,再验证。

Promise 的常见组合:

ts
const [user, orders] = await Promise.all([
  loadUser("1"),
  loadOrders("1")
]);
  • Promise.all:全部成功,任一失败则整体拒绝。
  • Promise.allSettled:等待全部完成并保留每项结果。
  • Promise.race:第一个完成或失败的结果。
  • Promise.any:第一个成功结果,全部失败时拒绝。

六、async/await

async 函数始终返回 Promise:

ts
async function getCount(): Promise<number> {
  return 3;
}

顺序执行:

ts
const user = await loadUser("1");
const orders = await loadOrders(user.id);

无依赖任务应并发启动:

ts
const userPromise = loadUser("1");
const configPromise = loadConfig();
const [user, config] = await Promise.all([userPromise, configPromise]);

七、异步取消与超时

Promise 本身没有通用取消语义。Web API 常用 AbortController

ts
async function fetchJson(url: string, timeoutMs: number): Promise<unknown> {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), timeoutMs);

  try {
    const response = await fetch(url, { signal: controller.signal });
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    return await response.json();
  } finally {
    clearTimeout(timer);
  }
}

类型只能描述 signal,不能替你设计取消后的状态恢复。

八、错误类型的现实限制

JavaScript 可以 throw 任意值,所以 catch 变量应按 unknown 处理:

ts
try {
  await doWork();
} catch (error: unknown) {
  if (error instanceof Error) {
    console.error(error.message);
  } else {
    console.error("未知错误", error);
  }
}

自定义错误类:

ts
class ValidationError extends Error {
  constructor(
    message: string,
    public readonly field: string
  ) {
    super(message);
    this.name = "ValidationError";
  }
}

九、Result 模式

对可预期业务失败,可用判别联合避免异常控制流:

ts
type Result<T, E> =
  | { ok: true; value: T }
  | { ok: false; error: E };

function parsePort(input: string): Result<number, string> {
  const value = Number(input);
  if (!Number.isInteger(value) || value < 1 || value > 65535) {
    return { ok: false, error: "端口范围应为 1~65535" };
  }
  return { ok: true, value };
}

选择原则:

  • 程序不变量破坏或无法恢复的异常:throw
  • 用户输入、业务拒绝等预期失败:Result 或明确返回值常更合适。
  • 不要机械地把所有第三方异步 API 包装成自制 Result,保持项目一致性。

十、外部数据校验

ts
interface User {
  id: number;
  name: string;
}

function parseUser(value: unknown): User {
  if (typeof value !== "object" || value === null) {
    throw new ValidationError("用户必须是对象", "user");
  }

  const record = value as Record<string, unknown>;
  if (typeof record.id !== "number") {
    throw new ValidationError("id 必须是数字", "id");
  }
  if (typeof record.name !== "string") {
    throw new ValidationError("name 必须是字符串", "name");
  }

  return { id: record.id, name: record.name };
}

大型项目通常采用 schema 校验库统一生成运行时校验和静态类型,避免手写守卫漂移。

十一、空值与集合查询

findMap.get、数组索引都可能没有结果:

ts
const order = orders.find(item => item.id === "C"); // Order | undefined
if (!order) {
  throw new Error("订单不存在");
}

不要通过非空断言掩盖:

ts
orders.find(item => item.id === "C")!.amount;

只有在结构性不变量已被前置逻辑证明时,非空断言才可作为局部说明。

十二、常见误区

  1. response.json() as User 被当作真实校验。
  2. catch 中假设错误一定是 Error
  3. 本可并行的 Promise 被连续 await
  4. 忘记清理计时器、监听器和取消控制器。
  5. 对预期业务失败全部使用异常。
  6. 对不可恢复错误全部吞掉并返回空值。
  7. 使用 ! 掩盖集合查询可能为空。
  8. 把 WeakMap 当作可枚举数据表。

十三、实践练习

  1. 实现 parseUser(unknown) 并覆盖错误输入测试。
  2. 并行加载用户与配置,比较连续和并发等待。
  3. 为 fetch 增加超时与取消,确保 finally 清理计时器。
  4. 用 Result 模式实现端口解析。
  5. 使用 Promise.allSettled 汇总多个供应商请求结果。

十四、总结

  1. 集合 API 的返回类型会显式暴露“可能不存在”。
  2. async 函数返回 Promise,类型不会自动设计并发、取消和超时。
  3. catch 输入应按 unknown 收窄。
  4. 预期失败可以用判别联合表达,不必全部抛异常。
  5. 外部数据进入可信领域模型之前必须执行运行时验证。
  6. 类型安全的异步代码仍需要资源清理、容量和错误策略。

请继续阅读:第6章:模块化工程。


原始资料引用


  • 第4章:类型建模
  • 第6章:模块化工程

本章目录
一、数组方法与类型推断二、Map 与普通对象三、Set、WeakMap 与 WeakSet四、迭代协议与生成器五、Promise 类型六、async/await七、异步取消与超时八、错误类型的现实限制九、Result 模式十、外部数据校验十一、空值与集合查询十二、常见误区十三、实践练习十四、总结原始资料引用Related Documents
苏ICP备2025204887号-2