clean-typescript-objects-data

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Clean Objects And Data

简洁对象与数据

Choose the shape that matches the job. Plain data is best for transfer, rendering, serialization, and pattern matching. Objects or classes are useful when behavior and invariants belong together.
选择匹配业务需求的结构。纯数据最适合传输、渲染、序列化和模式匹配。当行为与不变量需要结合时,对象或类会更有用。

OD1–OD2: Plain Data Versus Behavior

OD1–OD2: 纯数据 vs 行为

ts
// Good - behavior belongs with the object
interface Employee {
  calculatePay(): number;
}

class SalariedEmployee implements Employee {
  constructor(private readonly salary: number) {}
  calculatePay(): number {
    return this.salary;
  }
}
Use classes or objects with methods when they protect invariants or enable true polymorphism. Prefer plain data when values are mostly read, rendered, serialized, or passed across boundaries.
ts
// Good - behavior belongs with the object
interface Employee {
  calculatePay(): number;
}

class SalariedEmployee implements Employee {
  constructor(private readonly salary: number) {}
  calculatePay(): number {
    return this.salary;
  }
}
当类或对象需要保护不变量或实现真正的多态性时,使用带方法的类或对象。当值主要用于读取、渲染、序列化或跨边界传递时,优先选择纯数据。

OD3: Model Impossible States Out

OD3: 排除不可能的状态

Avoid bags of optional fields that can contradict each other.
ts
// Bad - many invalid combinations are possible
type Employee = {
  type: "SALARIED" | "HOURLY";
  salary?: number;
  hours?: number;
  rate?: number;
};

// Good - each variant has exactly the fields it needs
type Employee =
  | { type: "SALARIED"; salary: number }
  | { type: "HOURLY"; hours: number; rate: number };
避免使用存在相互矛盾的可选字段集合。
ts
// Bad - many invalid combinations are possible
type Employee = {
  type: "SALARIED" | "HOURLY";
  salary?: number;
  hours?: number;
  rate?: number;
};

// Good - each variant has exactly the fields it needs
type Employee =
  | { type: "SALARIED"; salary: number }
  | { type: "HOURLY"; hours: number; rate: number };

Exhaustive Dispatch

穷尽式分发

When dispatching on a discriminated union, make exhaustiveness compiler-enforced. Always include a
never
case that throws so adding a new union member breaks compilation until every dispatch site handles it.
ts
type Employee =
  | { type: "SALARIED"; salary: number }
  | { type: "HOURLY"; hours: number; rate: number }
  | { type: "COMMISSIONED"; base: number; commission: number };

function calculatePay(employee: Employee): number {
  switch (employee.type) {
    case "SALARIED":
      return employee.salary;
    case "HOURLY":
      return employee.hours * employee.rate;
    case "COMMISSIONED":
      return employee.base + employee.commission;
    default:
      return assertNever(employee);
  }
}

function assertNever(value: never): never {
  throw new Error(`Unhandled employee type: ${JSON.stringify(value)}`);
}
对Discriminated Unions进行分发时,确保编译器强制执行穷尽性检查。始终包含一个
never
分支并抛出错误,这样当添加新的联合成员时,所有分发位置都必须处理该成员才能通过编译。
ts
type Employee =
  | { type: "SALARIED"; salary: number }
  | { type: "HOURLY"; hours: number; rate: number }
  | { type: "COMMISSIONED"; base: number; commission: number };

function calculatePay(employee: Employee): number {
  switch (employee.type) {
    case "SALARIED":
      return employee.salary;
    case "HOURLY":
      return employee.hours * employee.rate;
    case "COMMISSIONED":
      return employee.base + employee.commission;
    default:
      return assertNever(employee);
  }
}

function assertNever(value: never): never {
  throw new Error(`Unhandled employee type: ${JSON.stringify(value)}`);
}

OD4: DTOs Versus Domain Models

OD4: DTO vs 领域模型

Keep external DTOs separate from domain models when names, nullability, units, or invariants differ. Convert at the boundary, then use domain types internally.
当外部DTO与领域模型在命名、可空性、单位或不变量上存在差异时,将它们分开。在边界处进行转换,内部使用领域类型。

OD5: Avoid Excessive Object-Chain Knowledge

OD5: 避免过度依赖对象链

ts
// Bad - reaching through another object's internals
const outputDir = context.options.scratchDir.absolutePath;

// Good - ask the owner object
const outputDir = context.getScratchDir();
Do not count dots mechanically. Optional chaining through API response data can be fine; coupling to another object's internals is the problem.
ts
// Bad - reaching through another object's internals
const outputDir = context.options.scratchDir.absolutePath;

// Good - ask the owner object
const outputDir = context.getScratchDir();
不要机械地统计点的数量。对API响应数据使用可选链是可以接受的;问题在于耦合到另一个对象的内部实现。

OD6: Represent Absence Explicitly

OD6: 显式表示缺失状态

Use
null
or
undefined
only when absence is an expected domain value and the caller can handle it plainly. Do not use
null
as a vague failure channel for parse errors, permission failures, missing configuration, or invalid state.
ts
// Bad - caller cannot tell why there is no user
function findUser(id: string): User | null {
  // ...
}

// Good - absence is the domain concept
function findUser(id: string): User | undefined {
  // ...
}
For recoverable failures, use the project's established result style or throw an error with context.
仅当缺失是预期的领域值且调用方可以直接处理时,才使用
null
undefined
。不要将
null
作为解析错误、权限失败、配置缺失或无效状态的模糊错误通道。
ts
// Bad - caller cannot tell why there is no user
function findUser(id: string): User | null {
  // ...
}

// Good - absence is the domain concept
function findUser(id: string): User | undefined {
  // ...
}
对于可恢复的错误,使用项目既定的结果样式或抛出带有上下文信息的错误。

OD7: Replace Repeated Conditionals

OD7: 替换重复条件判断

If the same
switch
, mode check, or type conditional appears in multiple places, the design knowledge is duplicated. Centralize it in a domain operation, lookup table, strategy, or exhaustive discriminated-union dispatch.
ts
// Bad - the same status branching spreads through the codebase
if (invoice.status === "paid") {
  // ...
}

// Good - one owner expresses the policy
if (canSendReceipt(invoice)) {
  // ...
}
如果相同的
switch
、模式检查或类型条件出现在多个位置,说明设计逻辑被重复了。将其集中到领域操作、查找表、策略模式或穷尽式Discriminated Unions分发中。
ts
// Bad - the same status branching spreads through the codebase
if (invoice.status === "paid") {
  // ...
}

// Good - one owner expresses the policy
if (canSendReceipt(invoice)) {
  // ...
}

Common Mistakes

常见错误

  • Using one type for API payloads, forms, database rows, and domain objects.
  • Adding optional fields instead of creating a new variant.
  • Using classes with no invariants, state, or polymorphic behavior.
  • Letting callers reach through nested objects instead of exposing the needed operation.
  • Returning
    null
    for several unrelated failure cases.
  • Copying the same status/type conditional into multiple modules.
  • 为API负载、表单、数据库行和领域对象使用同一种类型。
  • 添加可选字段而非创建新的变体类型。
  • 使用没有不变量、状态或多态行为的类。
  • 允许调用方通过嵌套对象访问内部属性,而非暴露所需的操作方法。
  • 为多个不相关的失败情况返回
    null
  • 将相同的状态/类型条件判断复制到多个模块中。