16 KiB
16 KiB
TypeScript 编码最佳实践(参考 Clean Code TypeScript)
本规范以 Clean Code TypeScript 为基础,结合常见业务场景与团队协作经验,为 TypeScript 项目提供可执行的最佳实践。所有规则都配有简短示例,便于在代码生成、评审与重构中快速对照。
1. 变量与常量
- 语义化命名:使用易读且准确的词汇描述变量含义,避免首字母缩写或无意义命名。
示例:const remainingTrialDays = totalTrialDays - usedTrialDays; - 统一词汇表述:同一概念使用一致命名,减少认知成本。
示例:type SubscriptionPlan = 'free' | 'pro'; const currentPlan: SubscriptionPlan = 'pro'; - 可搜索名称:对常量、配置使用具备辨识度的名称,方便全局检索。
示例:const DEFAULT_SESSION_TTL_MS = 30 * 60 * 1000; - 解释性变量:将复杂表达式抽取为具名变量,让意图自说明。
示例:const hasIncompleteProfile = !user.email || !user.phone; if (hasIncompleteProfile) { promptProfileCompletion(); } - 避免多余上下文:变量名中不要重复所在模块的上下文信息。
示例:// 数据存于 billing 模块,无需前缀 billing const invoiceTotal = calcInvoiceTotal(items); - 默认参数优于短路:使用函数默认值替代
||等短路写法,让可选参数更清晰。
示例:function sendReport(recipient: string, { retries = 3 }: { retries?: number } = {}) { /* ... */ }
2. 函数设计
- 函数只做一件事:将复杂操作拆分为独立函数,保持逻辑内聚。
示例:function createUser(dto: CreateUserInput) { validateUser(dto); const user = buildUser(dto); return saveUser(user); } - 限制参数数量:理想状态 ≤ 2 个参数;若超出,改用具名对象并定义类型。
示例:interface ScheduleOptions { triggerAt: Date; retryCount?: number; } function scheduleReminder(userId: string, options: ScheduleOptions) { /* ... */ } - 描述性函数名:函数名应说明行为与结果,避免依赖注释解释。
示例:function calculateLoyaltyDiscount(order: Order): number { /* ... */ } - 避免标志参数:用枚举或拆分函数替代布尔开关,消除逻辑分支迷雾。
示例:type ExportMode = 'draft' | 'final'; function exportDocument(mode: ExportMode) { /* ... */ } - 规避副作用:默认生成纯函数,必要副作用通过返回值或专门模块处理。
示例:function formatDisplayName(user: User): string { return `${user.firstName} ${user.lastName}`; } - 封装条件表达式:复杂条件抽离成具名函数或变量,降低认知负担。
示例:function isVipEligible(order: Order) { return order.total > 200 && order.customerTier === 'gold'; } if (isVipEligible(order)) { applyVipBenefits(order); } - 提前返回与正向逻辑:优先正向判断并提前返回,避免深层嵌套与否定条件。
示例:function ensureActive(user: User) { if (!user.isActive) { throw new Error('User is inactive'); } return user; } - 删除死代码:及时移除无调用、无覆盖的函数或分支,保持代码整洁。
示例:// 检测到无引用后移除旧方法 // function legacyPayment() { /* ... */ }
3. 对象与数据结构
- 使用访问器:通过 getter/setter 提供受控访问,便于增加校验与日志。
示例:class Account { constructor(private _balance = 0) {} get balance(): number { return this._balance; } deposit(amount: number) { this._balance += amount; } } - 隐藏实现细节:使用
private或#字段,阻止外部直接修改内部状态。
示例:class TokenStore { #tokens = new Map<string, string>(); set(token: string, value: string) { this.#tokens.set(token, value); } } - 偏好不可变数据:使用
Readonly<T>或解构复制,避免共享状态污染。
示例:const updatedOrder: Readonly<Order> = { ...order, status: 'shipped' }; - 类型守卫保障安全:自定义类型守卫封装校验逻辑,提升复用与可读性。
示例:function isAppConfig(value: unknown): value is AppConfig { return typeof value === 'object' && value !== null && 'env' in value; } - 利用字面量约束取值:针对有限集合使用联合字面量或
enum,防止魔法字符串。
示例:type OrderStatus = 'pending' | 'paid' | 'failed'; const status: OrderStatus = 'paid';
4. 类与面向对象
- 组合优先于继承:通过组合与接口扩展行为,降低继承层级带来的窜扰。
示例:class UserNotifier { constructor(private readonly emailSender: EmailSender) {} notifyWelcome(user: User) { return this.emailSender.sendWelcome(user.email); } } - 保持类职责单一:类只关注一个领域概念,额外能力拆分为协作类。
示例:class PasswordHasher { hash(value: string): Promise<string> { return bcrypt.hash(value, 12); } } - 接口驱动契约:用接口定义外部依赖,便于替换实现与注入假对象测试。
示例:interface EmailSender { sendWelcome(email: string): Promise<void>; } class SmtpEmailSender implements EmailSender { /* ... */ } - 方法链只在自然场景使用:当返回
this能提升易用性(如构建器)时才启用链式调用。
示例:class QueryBuilder { private filters: string[] = []; where(condition: string) { this.filters.push(condition); return this; } build() { return this.filters.join(' AND '); } } - 访问修饰符显式化:始终标识
public/private/protected,并优先使用readonly属性。
示例:class FeatureFlag { constructor(public readonly key: string, private readonly enabled: boolean) {} isEnabled() { return this.enabled; } }
5. SOLID 原则
- 单一职责(SRP):模块应聚焦唯一变更原因,降低耦合。
示例:class InvoicePrinter { constructor(private readonly formatter: InvoiceFormatter) {} print(invoice: Invoice) { return this.formatter.format(invoice); } } - 开放-封闭(OCP):通过扩展实现新需求,而非修改已有代码。
示例:interface PricingStrategy { calculate(order: Order): number; } class VipPricingStrategy implements PricingStrategy { /* ... */ } - 里氏替换(LSP):子类型必须兼容父类型契约,避免破坏预期。
示例:function printTotals(calculator: PricingStrategy, order: Order) { console.log(calculator.calculate(order)); } - 接口隔离(ISP):提供小而专注的接口,避免强迫消费者实现无关方法。
示例:interface ReadonlyRepository<T> { findById(id: string): Promise<T | null>; list(): Promise<T[]>; } - 依赖反转(DIP):高层模块依赖抽象,低层实现通过注入传入。
示例:class PaymentService { constructor(private readonly gateway: PaymentGateway) {} pay(invoice: Invoice) { return this.gateway.charge(invoice); } }
6. 异步与并发
- 优先使用 async/await:将 Promise 链写成同步流程,更易读并易于错误处理。
示例:export async function loadUser(id: string): Promise<User> { const response = await fetch(`/api/users/${id}`); return response.json(); } - 集中处理错误:在靠近调用边界处统一捕获和记录,保留上下文。
示例:try { await processPayment(orderId); } catch (error) { logger.error('Payment failed', { orderId, error }); throw new PaymentFailedError(orderId, error); } - 并发任务显式管理:使用
Promise.allSettled或自定义控制,避免静默丢失异常。
示例:const results = await Promise.allSettled(tasks.map(executeTask)); const failures = results.filter((r): r is PromiseRejectedResult => r.status === 'rejected'); - 支持超时与取消:利用
AbortController或第三方库防止长时间挂起。
示例:const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 5000); await fetch(url, { signal: controller.signal }); clearTimeout(timeoutId); - 避免竞争条件:在共享资源上使用锁、队列或幂等操作保障一致性。
示例:await lock.acquire('invoice:' + invoiceId, async () => { await issueInvoice(invoiceId); });
7. 错误处理
- 不忽略错误:捕获后要记录、包装或重新抛出,切勿空 catch。
示例:try { await cache.save(key, value); } catch (error) { metrics.increment('cache.save.failure'); throw error; } - 抛得早,捕获得晚:靠近错误发生点抛出,自上层统一处理。
示例:function ensureHasToken(token?: string): asserts token is string { if (!token) { throw new AuthError('Missing token'); } } - 提供上下文信息:传递自定义错误类型或消息,方便排查。
示例:class PaymentFailedError extends Error { constructor(public readonly orderId: string, cause: unknown) { super(`Payment failed for order ${orderId}`); this.name = 'PaymentFailedError'; this.cause = cause; } } - 使用
Result或Either模式(可选):在复杂流程中用显式返回承载错误,减少异常穿透。
示例:type Result<T> = { ok: true; value: T } | { ok: false; error: Error };
8. 格式化与一致性
- 自动化格式化:采用
prettier或团队约定,保持一致缩进与换行。
示例:{ "scripts": { "format": "prettier --write \"src/**/*.{ts,tsx}\"" } } - 启用严格 lint:结合
eslint与@typescript-eslint推行关键规则。
示例:{ "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"] } - 避免深层嵌套:通过提前返回或提取函数减少缩进层数。
示例:if (!user) { return null; } if (!user.isActive) { return null; } return user; - 统一命名风格:类型与类使用帕斯卡命名,函数与变量使用驼峰命名。
示例:class PaymentGateway {} function processPayment() { /* ... */ } - 集中导出入口:通过
index.ts组织导出,控制模块暴露面。
示例:export { processPayment } from './processPayment'; export type { PaymentRequest } from './types';
9. 注释与文档
- 注释意图而非实现:仅在业务规则或约束难以通过代码表达时补充说明。
示例:// 使用结算日的下一工作日作为默认扣款日期 const defaultChargeDate = getNextBusinessDay(settlementDate); - 使用 JSDoc/TS Doc:对外暴露的 API 写明输入、输出与异常。
示例:/** * 创建新的租户并返回租户 ID。 * @throws TenantLimitExceededError 当达到租户上限时抛出。 */ export async function createTenant(input: CreateTenantInput): Promise<string> { /* ... */ } - 不要注释掉代码:移除暂时不用的逻辑,依赖版本控制追踪历史。
示例:// 删除旧实现,改由 git 历史追溯 - 链接示例或测试:复杂模块在注释中指向参考用例或文档。
示例:// 更多用法见 tests/invoice/generateInvoice.test.ts generateInvoice(orderId);
10. 测试策略
- 安排-执行-断言 (AAA):测试结构清晰,便于阅读与维护。
示例:it('calculates total price', () => { const items = [{ price: 10 }, { price: 5 }]; // Arrange const result = totalPrice(items); // Act expect(result).toBe(15); // Assert }); - 一个测试一个概念:每个测试聚焦单一行为,失败时易于定位原因。
示例:it('throws when user is inactive', () => { expect(() => ensureActive({ isActive: false } as User)).toThrow(); }); - 利用假对象隔离依赖:通过实现接口的假对象或 mock 控制外部交互。
示例:class FakeEmailSender implements EmailSender { public sent: string[] = []; async sendWelcome(email: string) { this.sent.push(email); } } - 覆盖边界条件:针对 null、异常路径与类型缩小时的行为编写测试。
示例:it('returns empty list when no orders found', () => { expect(loadOrders([])).toEqual([]); }); - 类型驱动验证:关键类型转换或守卫应有对应测试,确保类型假设成立。
示例:it('accepts valid config objects', () => { const config = { env: 'prod' }; expect(isAppConfig(config)).toBe(true); });
## 11. 架构最佳实践
- **清晰分层**:按领域、应用、基础设施划分模块,控制依赖方向仅从外层指向内层。
示例:
```ts
// application/useCases/createInvoice.ts
import { Invoice } from '../domain/invoice';
import type { InvoiceRepository } from '../domain/ports';
- 组合根集中依赖注入:在应用入口集中装配依赖,避免在业务代码中到处
new实现。
示例:export function createInvoiceService(): InvoiceService { const repository = new PrismaInvoiceRepository(); return new InvoiceService(repository); } - 领域对象显式建模:使用类型与不可变对象表达核心概念,让业务规则留在领域层。
示例:type Invoice = Readonly<{ id: string; amount: number; issuedAt: Date; }>; - 边界 DTO 与映射:在接口适配层定义 DTO,与领域对象之间通过映射转换,保持领域纯净。
示例:type InvoiceDto = { id: string; amount: number; issued_at: string }; function toInvoice(dto: InvoiceDto): Invoice { return { id: dto.id, amount: dto.amount, issuedAt: new Date(dto.issued_at) }; } - 横切关注集中处理:将日志、缓存、鉴权等横切逻辑通过中间件或装饰器统一管理。
示例:export function withCaching<TArgs extends unknown[], TResult>( fn: (...args: TArgs) => Promise<TResult>, ) { return async (...args: TArgs) => cache.remember(JSON.stringify(args), () => fn(...args)); } - 配置与环境隔离:统一从配置模块读取环境变量,并提供类型安全的访问接口。
示例:export const config = { payment: { apiKey: process.env.PAYMENT_API_KEY ?? '', timeoutMs: Number(process.env.PAYMENT_TIMEOUT_MS ?? 5000), }, } as const;
附录:AI 辅助生成代码提示
- 在提示中植入约束:明确声明需使用
strictTypeScript、限制参数数量、禁止any。
示例:请实现 strict TypeScript 的订单接口,禁止 any,函数不超过 3 个参数,并写出必要类型定义。 - 要求输出结构化:提示 AI 同步生成类型、实现与测试用例。
示例:输出包含类型定义、主要函数实现,以及 Jest 测试示例。 - 生成后立即校验:运行
tsc --noEmit、eslint与测试脚本,验证生成代码质量。
示例:npx tsc --noEmit && npm run lint && npm test - 迭代式对话:先让 AI 生成数据结构与接口,再补实现与异常处理,最后检查测试通过。
示例:第一步:请先提供 API 接口类型定义。 第二步:基于上一步补充实现与错误处理。
按照本规范执行,可以在保证代码整洁的前提下,充分利用 TypeScript 的类型系统与语言特性,降低维护成本并提升交付质量。