clean-typescript-async

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Clean Async TypeScript

简洁的异步TypeScript

Async code should make ordering, ownership, and failure behavior explicit. Race-prone behavior deserves small units and direct tests.
异步代码应使执行顺序、资源归属和失败行为清晰明确。存在竞态风险的代码应拆分为小单元并进行直接测试。

A1: Isolate Async Workflows

A1:隔离异步工作流

Keep orchestration separate from pure transformation. Parse, validate, map, and calculate in synchronous helpers when possible; let async functions coordinate I/O.
ts
async function importUsers(file: FileHandle, repository: UserRepository) {
  const rows = await file.readRows();
  const users = rows.map(parseUserRow);
  await repository.saveMany(users);
}
将编排逻辑与纯转换逻辑分离。尽可能在同步辅助函数中进行解析、验证、映射和计算;让异步函数仅负责协调I/O操作。
ts
async function importUsers(file: FileHandle, repository: UserRepository) {
  const rows = await file.readRows();
  const users = rows.map(parseUserRow);
  await repository.saveMany(users);
}

A2: Make Ordering Explicit

A2:明确执行顺序

Use sequential
await
when order matters and
Promise.all
when operations are independent. Do not rely on array callbacks with hidden promise behavior.
ts
// Bad - promises are created but not awaited by forEach
users.forEach(async (user) => {
  await sendInvite(user);
});

// Good - independent work is explicit
await Promise.all(users.map((user) => sendInvite(user)));
当顺序重要时使用顺序
await
,当操作相互独立时使用
Promise.all
。不要依赖带有隐藏Promise行为的数组回调。
ts
// Bad - promises are created but not awaited by forEach
users.forEach(async (user) => {
  await sendInvite(user);
});

// Good - independent work is explicit
await Promise.all(users.map((user) => sendInvite(user)));

A3: Avoid Shared Mutable State Across Awaits

A3:避免在await边界间使用共享可变状态

Mutable variables that survive across
await
boundaries invite races and stale assumptions. Prefer local values, immutable updates, or a single owner for shared state.
ts
// Bad - concurrent calls can corrupt shared state
let cachedUser: User | undefined;

async function getUser(id: string) {
  if (!cachedUser) {
    cachedUser = await fetchUser(id);
  }
  return cachedUser;
}

// Good - cache key and mutation ownership are explicit
const userCache = new Map<string, Promise<User>>();

function getUser(id: string) {
  const existing = userCache.get(id);
  if (existing) {
    return existing;
  }

  const request = fetchUser(id).catch((error: unknown) => {
    userCache.delete(id);
    throw error;
  });
  userCache.set(id, request);
  return request;
}
If a cache stores promises, define the failure policy. Most request caches should evict rejected promises so one transient failure does not poison every future lookup.
跨越
await
边界的可变变量容易引发竞态条件和过期假设。优先使用局部值、不可变更新,或由单一所有者管理共享状态。
ts
// Bad - concurrent calls can corrupt shared state
let cachedUser: User | undefined;

async function getUser(id: string) {
  if (!cachedUser) {
    cachedUser = await fetchUser(id);
  }
  return cachedUser;
}

// Good - cache key and mutation ownership are explicit
const userCache = new Map<string, Promise<User>>();

function getUser(id: string) {
  const existing = userCache.get(id);
  if (existing) {
    return existing;
  }

  const request = fetchUser(id).catch((error: unknown) => {
    userCache.delete(id);
    throw error;
  });
  userCache.set(id, request);
  return request;
}
如果缓存存储Promise,需定义失败处理策略。大多数请求缓存应清除被拒绝的Promise,避免一次临时失败影响后续所有查询。

A4: Make Cancellation, Timeouts, Retries, And Fallbacks Visible

A4:显式处理取消、超时、重试和回退

Async failure policy is behavior. Keep retry counts, timeout durations, abort signals, and fallback choices named and near the call that owns the policy.
ts
const USER_LOOKUP_TIMEOUT_MS = 2_000;

await fetchUser(userId, {
  signal: AbortSignal.timeout(USER_LOOKUP_TIMEOUT_MS),
});
Do not hide broad fallbacks in shared clients unless every caller truly wants the same behavior.
异步失败策略是核心行为。将重试次数、超时时长、中止信号和回退选项命名,并放在负责该策略的调用附近。
ts
const USER_LOOKUP_TIMEOUT_MS = 2_000;

await fetchUser(userId, {
  signal: AbortSignal.timeout(USER_LOOKUP_TIMEOUT_MS),
});
不要在共享客户端中隐藏通用回退逻辑,除非所有调用方确实需要相同的行为。

A5: Test Race-Prone Behavior

A5:测试存在竞态风险的行为

When code depends on ordering, cancellation, retry, timeout, or deduplication, test that behavior directly with controlled promises, fake timers, or in-memory fakes.
ts
test("deduplicates concurrent user fetches", async () => {
  const fetchUser = vi.fn().mockResolvedValue({ id: "user-1" });
  const users = createUserLoader(fetchUser);

  await Promise.all([users.get("user-1"), users.get("user-1")]);

  expect(fetchUser).toHaveBeenCalledTimes(1);
});
Flaky async tests are usually a design signal: make the event, clock, promise, or external dependency controllable.
当代码依赖于执行顺序、取消、重试、超时或去重逻辑时,使用受控Promise、模拟计时器或内存模拟直接测试这些行为。
ts
test("deduplicates concurrent user fetches", async () => {
  const fetchUser = vi.fn().mockResolvedValue({ id: "user-1" });
  const users = createUserLoader(fetchUser);

  await Promise.all([users.get("user-1"), users.get("user-1")]);

  expect(fetchUser).toHaveBeenCalledTimes(1);
});
不稳定的异步测试通常是设计问题的信号:需使事件、时钟、Promise或外部依赖可控制。