Skip to content
aipatterns.dev

Refactoring techniqueOrganizing Data

Replace Type Code with Class

Replace a set of string or number codes that don't affect behavior with a dedicated type, so only valid codes can be used.

Problem

A field holds one of a fixed set of codes, such as "low" | "normal" | "urgent" stored as a plain string, or 1, 2, 3 as number. Any string compiles, typos slip through, and facts about each code (its label, its sort order) are scattered across the codebase.

Solution

Create a type whose only instances are the valid codes, and move the data attached to each code into it. Use this variant when the code does not drive conditional behavior; if it does, see the subclass or State/Strategy variants.

Before

TypeScript
class Ticket {
  constructor(public title: string, public priority: string) {}
}

function sortQueue(tickets: Ticket[]): Ticket[] {
  const rank: Record<string, number> = { urgent: 0, normal: 1, low: 2 };
  return [...tickets].sort((a, b) => rank[a.priority] - rank[b.priority]);
}

function badge(t: Ticket): string {
  return t.priority === "urgent" ? "P1" : t.priority === "normal" ? "P2" : "P3";
}

new Ticket("Login broken", "urgnet"); // compiles

After

TypeScript
class Priority {
  static readonly Urgent = new Priority("urgent", 0, "P1");
  static readonly Normal = new Priority("normal", 1, "P2");
  static readonly Low = new Priority("low", 2, "P3");
  private static readonly all = [Priority.Urgent, Priority.Normal, Priority.Low];

  private constructor(readonly code: string, readonly rank: number, readonly badge: string) {}

  static fromCode(code: string): Priority {
    const p = Priority.all.find((x) => x.code === code);
    if (!p) throw new Error(`unknown priority ${code}`);
    return p;
  }
}

class Ticket {
  constructor(public title: string, public priority: Priority) {}
}

const sortQueue = (tickets: Ticket[]) =>
  [...tickets].sort((a, b) => a.priority.rank - b.priority.rank);
const badge = (t: Ticket) => t.priority.badge;

Why

  • Invalid codes are rejected at the boundary (fromCode) and impossible in code.
  • Rank and badge live with the code. Adding a priority means one new line.
  • Signatures say Priority, not string.

Steps

  1. Create the class with a private constructor and one static instance per code. Keep the raw code as a field for persistence.
  2. Add a fromCode factory that validates.
  3. Change the owner's field type. Convert at the boundaries: fromCode when loading, .code when saving. Run the tests.
  4. Move per-code data (labels, ranks, colors) from lookup tables into the class. Run the tests after each move.
  5. Replace remaining string comparisons with instance comparisons (=== Priority.Urgent).

Watch out

  • In TypeScript, a string literal union (type Priority = "urgent" | "normal" | "low") already gives compile-time safety with no runtime cost. Prefer it when there is no attached data. Other languages have enum with fields (Java, Kotlin, Rust, Swift); use those.
  • If code branches on the type to behave differently, this is only a first step. Continue with Replace Type Code with Subclasses or Replace Type Code with State/Strategy.
  • Persisted values must stay the same strings or numbers. Serialize code, never the object.
  • Inverse: go back to a union type or plain constants with Replace Magic Number with Symbolic Constant when the class carries no data.

Agent checklist

  • The codes do not drive behavior through conditionals; otherwise a sibling technique fits.
  • A language-native enum or union type was considered first.
  • Only valid instances can be constructed; parsing validates.
  • Persistence still stores the original code values.
  • Tests pass.

Treats these smells

See also

Original writing and examples. Topic coverage follows Refactoring.Guru, which explains this topic with its own illustrations.