clean-typescript-modules

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Clean TypeScript Modules

整洁的TypeScript模块

Modules should read as small, cohesive units. Keep related code close, expose one clear concept, and avoid abstractions that do not carry real responsibility.
模块应作为小巧、内聚的单元来编写。将相关代码放在一起,只暴露一个清晰的核心概念,避免没有实际职责的抽象。

M1: Keep Declarations Close To Use

M1:将声明放在靠近使用的位置

Declare variables near the code that needs them. A value defined at the top of a function but used much later forces the reader to remember stale context.
ts
// Bad - formatter is irrelevant until much later
function sendDigest(users: User[], now: Date) {
  const formatter = new Intl.DateTimeFormat("en-US");

  const activeUsers = users.filter((user) => user.active);
  const recipients = activeUsers.map((user) => user.email);
  const subject = `Digest for ${recipients.length} users`;

  return {
    recipients,
    subject,
    generatedAt: formatter.format(now),
  };
}

// Good - declaration sits with the idea that uses it
function sendDigest(users: User[], now: Date) {
  const activeUsers = users.filter((user) => user.active);
  const recipients = activeUsers.map((user) => user.email);
  const subject = `Digest for ${recipients.length} users`;
  const formatter = new Intl.DateTimeFormat("en-US");

  return {
    recipients,
    subject,
    generatedAt: formatter.format(now),
  };
}
Module-level constants are fine when they are shared by multiple functions or define a true policy. Do not lift locals only to make a function look shorter.
在需要使用变量的代码附近声明变量。如果在函数顶部定义一个值,但很久之后才使用,会迫使读者记住早已过时的上下文信息。
ts
// Bad - formatter is irrelevant until much later
function sendDigest(users: User[], now: Date) {
  const formatter = new Intl.DateTimeFormat("en-US");

  const activeUsers = users.filter((user) => user.active);
  const recipients = activeUsers.map((user) => user.email);
  const subject = `Digest for ${recipients.length} users`;

  return {
    recipients,
    subject,
    generatedAt: formatter.format(now),
  };
}

// Good - declaration sits with the idea that uses it
function sendDigest(users: User[], now: Date) {
  const activeUsers = users.filter((user) => user.active);
  const recipients = activeUsers.map((user) => user.email);
  const subject = `Digest for ${recipients.length} users`;
  const formatter = new Intl.DateTimeFormat("en-US");

  return {
    recipients,
    subject,
    generatedAt: formatter.format(now),
  };
}
当模块级常量被多个函数共享或定义了明确的规则时,将其放在模块顶部是可行的。不要仅仅为了让函数看起来更短就把局部变量提升到模块级。

M2: Order Code For Top-Down Reading

M2:按自上而下的顺序组织代码

Put the public operation first, then the private helpers it uses. A reader should be able to skim the file from high-level intent to implementation detail.
ts
export function priceOrder(order: Order): Money {
  return applyDiscounts(calculateSubtotal(order), order.discounts);
}

function calculateSubtotal(order: Order): Money {
  // ...
}

function applyDiscounts(subtotal: Money, discounts: Discount[]): Money {
  // ...
}
Prefer local consistency when the project already has a strong file-order convention.
先放置公共操作,再放置它所依赖的私有辅助函数。读者应该能够从文件的开头到结尾,从高层意图逐步深入到实现细节。
ts
export function priceOrder(order: Order): Money {
  return applyDiscounts(calculateSubtotal(order), order.discounts);
}

function calculateSubtotal(order: Order): Money {
  // ...
}

function applyDiscounts(subtotal: Money, discounts: Discount[]): Money {
  // ...
}
如果项目已经有明确的文件顺序约定,优先遵循本地一致性。

M3: Keep Modules Cohesive

M3:保持模块的内聚性

A module should have one reason to change. Split files that mix unrelated policies, infrastructure, DTO mapping, rendering helpers, and domain calculations.
ts
// Bad - one file changes for pricing, HTTP, and email policy
export async function fetchOrder() {}
export function calculateOrderTotal() {}
export function formatReceiptEmail() {}

// Good - each file has a focused reason to change
// order-client.ts, order-pricing.ts, receipt-email.ts
一个模块应该只有一个变更理由。拆分那些混合了无关规则、基础设施、DTO映射、渲染辅助函数和领域计算的文件。
ts
// Bad - one file changes for pricing, HTTP, and email policy
export async function fetchOrder() {}
export function calculateOrderTotal() {}
export function formatReceiptEmail() {}

// Good - each file has a focused reason to change
// order-client.ts, order-pricing.ts, receipt-email.ts

M4: Keep Dependency Direction Obvious

M4:让依赖方向清晰可见

Higher-level domain code should not depend on low-level details unless that detail is the domain. Pass behavior through small interfaces or functions when it prevents vendor or infrastructure coupling.
ts
// Bad - domain calculation knows the SDK shape
function calculateInvoiceTotal(invoice: Stripe.Invoice): number {
  return invoice.lines.data.reduce((total, line) => total + line.amount, 0);
}

// Good - boundary code maps vendor data into domain data first
function calculateInvoiceTotal(invoice: Invoice): number {
  return invoice.lines.reduce((total, line) => total + line.amountCents, 0);
}
This complements boundary rules: validate and map external data at the edge, then keep inner modules stable.
高层领域代码不应依赖底层细节,除非该细节本身就是领域的一部分。当需要避免与供应商或基础设施耦合时,通过小型接口或函数传递行为。
ts
// Bad - domain calculation knows the SDK shape
function calculateInvoiceTotal(invoice: Stripe.Invoice): number {
  return invoice.lines.data.reduce((total, line) => total + line.amount, 0);
}

// Good - boundary code maps vendor data into domain data first
function calculateInvoiceTotal(invoice: Invoice): number {
  return invoice.lines.reduce((total, line) => total + line.amountCents, 0);
}
这与边界规则相辅相成:在边缘层验证并映射外部数据,然后保持内部模块的稳定性。

M5: Avoid Empty Abstractions

M5:避免空抽象

Do not add wrappers, managers, services, base classes, or helpers that only rename one call without hiding complexity, protecting invariants, or improving a real boundary.
ts
// Bad - no responsibility, just another place to click through
function getUserName(user: User): string {
  return user.name;
}

// Good - the abstraction carries a domain rule
function getDisplayName(user: User): string {
  return user.preferredName ?? user.name;
}
Delete abstractions that no longer earn their place. Small direct code is cleaner than a maze of thin indirection.
不要添加仅重命名单一调用、却没有隐藏复杂度、保护不变量或优化实际边界的包装器、管理器、服务、基类或辅助函数。
ts
// Bad - no responsibility, just another place to click through
function getUserName(user: User): string {
  return user.name;
}

// Good - the abstraction carries a domain rule
function getDisplayName(user: User): string {
  return user.preferredName ?? user.name;
}
删除那些不再有存在价值的抽象。简洁直接的代码比层层嵌套的薄间接层更清晰。

M6: Separate Construction From Use

M6:将构建与使用分离

Keep dependency construction, environment/config reads, SDK clients, database connections, clocks, random generators, and other external state near application boundaries. Domain behavior should receive ready-to-use dependencies instead of constructing them internally.
ts
// Bad - business behavior is mixed with construction and config
async function sendReceipt(order: Order) {
  const payments = new StripePayments(process.env.STRIPE_KEY);
  const emailer = new SendGridEmailer(process.env.SENDGRID_KEY);

  const receipt = await payments.createReceipt(order);
  await emailer.send(order.customerEmail, receipt);
}

// Good - construction happens at the edge; behavior uses explicit dependencies
async function sendReceipt(order: Order, payments: Payments, emailer: Emailer) {
  const receipt = await payments.createReceipt(order);
  await emailer.send(order.customerEmail, receipt);
}
Do not add dependency injection ceremony for simple values or harmless local objects. This rule matters most when construction touches I/O, config, time, randomness, vendor SDKs, persistence, or anything that makes behavior hard to test or change.
将依赖构建、环境/配置读取、SDK客户端、数据库连接、时钟、随机生成器和其他外部状态放在应用程序的边缘层。领域行为应接收现成可用的依赖,而不是在内部构建它们。
ts
// Bad - business behavior is mixed with construction and config
async function sendReceipt(order: Order) {
  const payments = new StripePayments(process.env.STRIPE_KEY);
  const emailer = new SendGridEmailer(process.env.SENDGRID_KEY);

  const receipt = await payments.createReceipt(order);
  await emailer.send(order.customerEmail, receipt);
}

// Good - construction happens at the edge; behavior uses explicit dependencies
async function sendReceipt(order: Order, payments: Payments, emailer: Emailer) {
  const receipt = await payments.createReceipt(order);
  await emailer.send(order.customerEmail, receipt);
}
不要为简单值或无害的本地对象添加依赖注入的繁琐流程。当构建涉及I/O、配置、时间、随机性、供应商SDK、持久化或任何会让行为难以测试或修改的内容时,这条规则最为重要。

M7: Avoid Temporal Coupling

M7:避免时序耦合

Avoid APIs that require callers to remember a hidden sequence such as
init()
, then
load()
, then
run()
. Return ready-to-use objects from factories, or model the states explicitly so invalid order is unrepresentable.
ts
// Bad - caller must know the required order
const importer = new Importer();
await importer.connect();
await importer.loadSchema();
await importer.run(file);

// Good - setup returns the usable dependency
const importer = await createImporter(config);
await importer.run(file);
避免要求调用者记住隐藏执行顺序的API,例如先
init()
、再
load()
、最后
run()
。从工厂函数返回现成可用的对象,或者显式建模状态,使无效的调用顺序无法被表达。
ts
// Bad - caller must know the required order
const importer = new Importer();
await importer.connect();
await importer.loadSchema();
await importer.run(file);

// Good - setup returns the usable dependency
const importer = await createImporter(config);
await importer.run(file);

M8: Keep Public Exports Small And Intentional

M8:保持公共导出精简且明确

Every export becomes part of the module's design surface. Export the operation, type, or component callers are meant to use; keep private helpers private until another owner has a real need for them.
ts
// Bad - implementation details become dependencies
export function normalizeLineItem() {}
export function calculateSubtotal() {}
export function applyInvoiceDiscounts() {}

// Good - one intentional public operation
export function calculateInvoiceTotal() {}
每个导出都会成为模块设计面的一部分。只导出调用者需要使用的操作、类型或组件;将私有辅助函数保持私有,直到其他模块有真正的需求时再导出。
ts
// Bad - implementation details become dependencies
export function normalizeLineItem() {}
export function calculateSubtotal() {}
export function applyInvoiceDiscounts() {}

// Good - one intentional public operation
export function calculateInvoiceTotal() {}