Tree-shaped data is represented as untyped records with a discriminating field; every operation walks the tree by switching on that field. Adding a new node kind requires editing every operation; adding a new operation requires re-implementing the switch.
Untyped tree records the agent must reason about as discriminated unions enforced by convention, not by type. Every traversal is a switch the agent verifies for exhaustiveness; static analysis cannot prove no kind was forgotten.
Leaf and composite share an interface; the composite's implementation of any operation delegates to its children. Client code treats trees and leaves uniformly; new node kinds are new classes; new operations are new methods.
A typed interface where adding a new node kind forces the type system to demand an implementation of every operation. The agent's edit-verify cycle on a new kind is bounded by the interface; static analysis returns complete results.
Before the pattern
function totalSize(node) {if (node.type === 'file') {return node.bytes;} else if (node.type === 'directory') {let total = 0;for (const child of node.children) {total += totalSize(child);}return total;}throw new Error(`unknown node type: ${node.type}`);}function nameOf(node) {if (node.type === 'file' || node.type === 'directory') return node.name;throw new Error(`unknown node type: ${node.type}`);}// Every traversal repeats the if/else; new node types touch every function.
After the pattern
class FileNode {constructor(name, bytes) {this.name = name;this.bytes = bytes;}totalSize() { return this.bytes; }}class DirectoryNode {constructor(name, children) {this.name = name;this.children = children;}totalSize() {return this.children.reduce((sum, child) => sum + child.totalSize(), 0);}}// Client treats leaf and composite uniformly:const root = new DirectoryNode('project', [new FileNode('README.md', 1024),new DirectoryNode('src', [new FileNode('index.js', 2048)]),]);const size = root.totalSize();
Repeated Switches at every traversal: totalSize switches on type, nameOf switches on type, render switches on type. Each switch grows in lockstep when a new kind ships; one missed switch ships as a runtime error on a code path the test suite may not exercise.
Discriminated-record traversals scatter the kind-handling logic across every operation; the agent's reasoning load grows with operation count × kind count. Missed branches surface as runtime exceptions on paths the test suite did not happen to exercise.
Leaf and composite must agree on the interface even when an operation makes no sense on one of them — leaves often implement add()/remove() child operations as throws or no-ops, exposing the divergence in shape behind the uniform interface. The pattern is at its sharpest when every operation is meaningful on both sides; it loses some elegance otherwise.
Uniform interfaces force the agent to recognize 'this operation is a no-op on leaves' or 'this throws on leaves' as patterns separate from the interface itself. Stack traces from a leaf rejecting add() require the agent to trace through the interface contract to understand why the rejection happened.
totalSize() is one expression: 'sum the children'; client code reads as `root.totalSize()` regardless of how deep the tree is. New node kinds land as new classes without editing existing traversals; new operations land as new methods without editing client code.
Per-kind edits scope to one class file; per-operation edits scope to adding one method across N classes (mechanical, scriptable). The composite traversal is a one-line reduce; recursion verification is local to the composite class.
When leaf and composite genuinely diverge in their useful operations — composite needs add/remove, leaf does not — the uniform interface forces leaves to expose meaningless operations or throw on them. The transparency-vs-safety tradeoff (uniform-but-unsafe vs typed-but-discriminated) is real; pick deliberately.
Leaves implementing add()/remove() as throws turns the type system's promise of uniformity into a runtime trap. The agent reading the interface expects the operation to work; the runtime experience contradicts the static read. Prefer the discriminated-union form when leaf and composite diverge on more than two operations.