Compare
Symptom
Human

An integer or string field encodes a domain concept; every consumer of the field knows the magic values and branches on them. New status values land as new magic literals; comparisons duplicate across the codebase; the type system treats `status === 2` and `status === 'shipped'` as equally valid.

Agent

Magic-literal comparisons across N consumers the agent must verify match a documented (or undocumented) set of valid values. The type system cannot enforce membership; typos and stale literals ship silently to runtime.

Goal
Human

The type code becomes a value object class with named instances. Comparisons become method calls (`status.canCancel()`); new statuses land as new instances + tests; the type system distinguishes order statuses from arbitrary integers.

Agent

Static type-checking enforces that comparisons are only against the named instances. The agent verifies consumers by class membership; per-status behaviour is locally readable as methods on the class.

Before the refactoring

// Status is an integer; every consumer comparisons it against magic literals.
class Order {
constructor() {
this.status = 0; // 0=pending, 1=paid, 2=shipped, 3=delivered, 4=cancelled
}
describe() {
if (this.status === 0) return 'Pending';
if (this.status === 1) return 'Paid';
if (this.status === 2) return 'Shipped';
if (this.status === 3) return 'Delivered';
if (this.status === 4) return 'Cancelled';
return 'Unknown';
}
canCancel() {
return this.status === 0 || this.status === 1;
}
}

After the refactoring

// Status is a typed value object; comparisons and behaviour live on the class.
class OrderStatus {
static PENDING = new OrderStatus('Pending', true);
static PAID = new OrderStatus('Paid', true);
static SHIPPED = new OrderStatus('Shipped', false);
static DELIVERED = new OrderStatus('Delivered', false);
static CANCELLED = new OrderStatus('Cancelled', false);
constructor(label, cancellable) {
this.label = label;
this.cancellable = cancellable;
}
describe() { return this.label; }
canCancel() { return this.cancellable; }
}
class Order {
constructor() {
this.status = OrderStatus.PENDING;
}
describe() { return this.status.describe(); }
canCancel() { return this.status.canCancel(); }
}
Example source: Illustrative example written for this site, faithful to Kerievsky's pattern shape in Refactoring to Patterns (Addison-Wesley, 2004), chapter 9. Distinct from Fowler's Replace Type Code With Subclasses (which makes the type code drive a hierarchy): this pattern is the lower-ceremony first move — turn the primitive into a value object that can carry methods and validation; subclasses come later if behaviour diverges enough to warrant them.
Pressure
Human

Magic-literal type codes are Primitive Obsession at scale. Bugs ship as 'used 2 when 3 was intended' or 'compared status to lowercase 'shipped' when the convention is uppercase'. The compiler/runtime cannot help; tests and code review carry the full enforcement burden.

Agent

Magic literals defeat static analysis at every comparison site. The agent's verification cost on a new status × M consumers scales with consumer count; cross-consumer consistency requires holistic test coverage that may not exist.

Tradeoff
Human

A value object class is one more domain class to learn; serializing/deserializing requires explicit translation between the primitive and the value object form. For status fields that flow through many database round-trips, the translation boundary is real work.

Agent

Value-object instances are reference-equality-checked in JavaScript; serialization round-trips require explicit handling. The agent must verify that deserialization produces the same canonical instances, not new equivalents, or `===` comparisons silently fail.

Relief
Human

Comparisons become typed method calls; new statuses are one named instance with one row of test coverage; consumers don't need to know the underlying primitive at all. Adding behaviour to a status (e.g., 'is terminal?') is one method on the class.

Agent

Adding a new status is one new subclass; the type checker confirms consumers handle every variant, and per-status behavior lives on the variant's class instead of in switch branches across every consumer.

Trap
Human

If the value object simply wraps the primitive without offering methods (no `describe`, no `canCancel`, no `isTerminal`), it adds a layer without buying anything. The pattern earns its keep when the type code carries behaviour worth promoting — not when it's just a name for the primitive.

Agent

Value objects relying on reference equality across serialization boundaries (HTTP, persistence, message queues) require careful canonicalization; getting it wrong produces runtime equality bugs the agent cannot detect statically. The pattern is straightforward in pure-runtime code; thornier across persistence boundaries.