Symptom

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.

Goal

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.

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

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.

Tradeoff

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.

Relief

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.

Trap

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.