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.
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.
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.
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(); }}
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.
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.
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.
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.
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.
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.
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.
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.