clean-typescript-modules
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseClean 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.tsM4: 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 , then , then . Return ready-to-use objects from factories, or model the states explicitly so invalid order is unrepresentable.
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);避免要求调用者记住隐藏执行顺序的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() {}