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'.
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.
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.
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.
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?'
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.