Composite
Compose objects into tree structures and treat individual objects and compositions uniformly. Client code can work with a single leaf or an entire tree using the exact same interface.
The Problem
You’re building a file system. A File has a size. A Directory contains File objects and other Directory objects — and its size is the sum of everything inside it. Client code that wants to calculate total size ends up with:
// Ugly: client must check types and recurse manually
function getSize(item: File | Directory): number {
if (item instanceof Directory) {
return item.children.reduce((sum, child) => sum + getSize(child), 0);
}
return item.size;
}
This forces clients to know whether they have a leaf or a composite — violating the principle that client code shouldn’t care.
The Solution
Define a common Component interface with an operation (e.g. getSize()). Both leaves and composites implement it. Composites delegate to their children. Clients call the same method on anything — they never check the type.
Real-World Analogy
An organisational chart. A single employee has a salary. A department’s budget is the sum of all employees in it, including nested sub-departments. You can ask “what’s the cost of this team?” at any level — the same question works whether you’re pointing at one person or the entire company.
TypeScript Example
// Component interface — the uniform API
interface FileSystemItem {
getName(): string;
getSize(): number;
print(indent?: string): void;
}
// Leaf — no children
class File implements FileSystemItem {
constructor(
private readonly name: string,
private readonly size: number
) {}
getName(): string { return this.name; }
getSize(): number { return this.size; }
print(indent = ''): void {
console.log(`${indent}📄 ${this.name} (${this.size} KB)`);
}
}
// Composite — contains other items (leaves or composites)
class Directory implements FileSystemItem {
private children: FileSystemItem[] = [];
constructor(private readonly name: string) {}
add(item: FileSystemItem): this {
this.children.push(item);
return this;
}
remove(item: FileSystemItem): void {
const index = this.children.indexOf(item);
if (index !== -1) this.children.splice(index, 1);
}
getName(): string { return this.name; }
// Delegates to children — client doesn't recurse manually
getSize(): number {
return this.children.reduce((sum, child) => sum + child.getSize(), 0);
}
print(indent = ''): void {
console.log(`${indent}📁 ${this.name}/ (${this.getSize()} KB total)`);
this.children.forEach((child) => child.print(indent + ' '));
}
}
// Build a tree
const root = new Directory('project')
.add(new File('package.json', 2))
.add(new File('README.md', 4))
.add(
new Directory('src')
.add(new File('index.ts', 8))
.add(
new Directory('components')
.add(new File('Button.tsx', 12))
.add(new File('Modal.tsx', 18))
)
)
.add(
new Directory('tests')
.add(new File('index.test.ts', 6))
);
root.print();
/*
📁 project/ (50 KB total)
📄 package.json (2 KB)
📄 README.md (4 KB)
📁 src/ (38 KB total)
📄 index.ts (8 KB)
📁 components/ (30 KB total)
📄 Button.tsx (12 KB)
📄 Modal.tsx (18 KB)
📁 tests/ (6 KB total)
📄 index.test.ts (6 KB)
*/
// Client code — treats File and Directory identically
function printSize(item: FileSystemItem): void {
console.log(`"${item.getName()}" is ${item.getSize()} KB`);
}
printSize(new File('tiny.js', 1)); // leaf
printSize(root); // composite — same call!
When to Use
- You need to represent part-whole hierarchies (trees)
- You want clients to treat individual objects and compositions of objects uniformly
- The structure has recursive nesting of the same type of object
- Examples: file systems, UI component trees, org charts, menu hierarchies, AST nodes, HTML DOM
Pros
- Client code is simpler — one interface for all nodes, no type checking needed
- Adding new component types (new leaf or composite variant) requires no changes to client code
- Operations on composites naturally recurse through the tree
Cons
- Can make the overall design overly general — hard to restrict a composite to only certain component types
- Some operations make sense only on leaves or only on composites — the shared interface can become awkward
Related Patterns
- Iterator lets you traverse composite trees without exposing their internal structure
- Visitor lets you execute an operation on every component in a composite tree
- Builder is commonly used to construct complex Composite trees step by step
- Decorator adds behaviour to a single component; Composite aggregates a tree of components