Every operation that touches a value first checks 'is this one or many?' via `Array.isArray`, type checks, or flag fields. The branches duplicate across operations; adding a new operation means another pair of branches; the abstraction 'a field has values' fragments into 'a field has a value OR has values'.
Per-operation `Array.isArray` branches the agent must verify match across N operations. The agent cannot statically guarantee that every operation handles both shapes consistently; subtle inconsistencies surface only when a one-only operation receives a many value.
Single and many implement the same interface (Composite). Callers send the same message — `.asString()`, `.isMultivalued()` — and the polymorphic receiver responds correctly. The one-vs-many decision lives once, in the construction of the appropriate type.
Polymorphic dispatch on the value's type; the agent verifies each subtype's implementation in isolation. Per-operation analysis no longer requires holding 'what shape is this value?' in working memory.
Before the refactoring
// Every operation branches on whether the value is one or many.class Form {setValue(name, valueOrValues) {if (Array.isArray(valueOrValues)) {this.fields[name] = valueOrValues.map((v) => sanitize(v));} else {this.fields[name] = sanitize(valueOrValues);}}getDisplay(name) {const value = this.fields[name];if (Array.isArray(value)) {return value.join(', ');}return value;}isMultivalued(name) {return Array.isArray(this.fields[name]);}}
After the refactoring
// Both single and many implement the same Field interface; callers stop branching.class Field {asString() { throw new Error('abstract'); }isMultivalued() { return false; }}class SingleField extends Field {constructor(raw) {super();this.value = sanitize(raw);}asString() { return this.value; }}class MultiField extends Field {constructor(items) {super();this.children = items.map((item) => new SingleField(item));}asString() { return this.children.map((child) => child.asString()).join(', '); }isMultivalued() { return true; }}class Form {setValue(name, field) {this.fields[name] = field; // already a SingleField or MultiField}getDisplay(name) {return this.fields[name].asString();}isMultivalued(name) {return this.fields[name].isMultivalued();}}
One-vs-many branches multiply with operation count. Subtle inconsistencies between operations (`getDisplay` handles arrays, but `getCount` forgets to) ship as silent bugs. The 'is it an array?' check pollutes every call site that touches the value.
One-vs-many branch checks duplicate across every consumer; the agent's per-edit verification cost scales with operation count. Static type information is shallow when JavaScript allows both shapes to flow through the same variable.
Composite requires constructing the right subtype at the boundary where the value enters the system; serialization round-trips may need explicit one-vs-many encoding. For two operations on a stable one-vs-many distinction, the inline branches may be cheaper than the type hierarchy.
Composite spreads behaviour across a type hierarchy; the agent must traverse subtypes to know what a method does for a given value. Subtype construction at boundaries is itself a place the agent must verify the one-vs-many decision lands correctly.
Operations read as polymorphic calls; the one-vs-many decision is made once and forgotten. Adding an operation is one method on Field with one implementation per subtype; the agent and reader never re-derive 'is it one or many?'
Adding a new operation is one method on the Composite interface plus one implementation per subtype; the type checker confirms every subtype implements the operation, and operation bodies no longer branch on one-versus-many because the Composite handles the shape.
A Composite hierarchy applied to a value that is *truly* always one or always many adds an indirection without buying anything. The pattern earns its keep when the same call site handles both cases — when the call site itself doesn't care, the polymorphism is the point.
A Composite applied to a distinction that is actually load-bearing context (e.g., when callers need to know cardinality to render differently) ends up restoring the `if (isMulti)` branches at the call site, just with `.isMultivalued()` instead of `Array.isArray`. The pattern pays when callers genuinely don't care which shape they have.