Composite Pattern
Composes objects into tree structures to represent part-whole hierarchies. Composite lets clients treat individual objects and compositions of objects uniformly through a shared interface, making recursive tree structures easy to build and traverse.
✦ Problem
In many systems, you deal with tree-like structures (menus, file systems, organizational charts). Using these structures only makes sense if the client code can treat both simple elements and complex containers in the same way. If the client needs type-checking to distinguish leaves from branches, the code becomes riddled with conditionals and hard to extend.
✦ Solution
Define a common interface for both simple and complex elements. Simple elements (leaves) implement behavior directly. Complex elements (composites) hold a collection of child elements and delegate work by iterating over their children. The client uses the same interface to interact with all elements regardless of complexity.
Participants
| Role | Responsibility |
|---|---|
| Component | Declares the common interface for objects in the composition |
| Leaf | Represents leaf objects with no children; defines behavior for primitive objects |
| Composite | Stores child components and implements child-related operations; delegates to children |
| Client | Manipulates objects in the composition through the Component interface |
TypeScript Example
interface FileSystemNode {
name: string;
getSize(): number;
print(indent?: string): string;
}
class File implements FileSystemNode {
constructor(public name: string, private size: number) {}
getSize() { return this.size; }
print(indent = "") {
return `${indent}📄 ${this.name} (${this.size}KB)`;
}
}
class Directory implements FileSystemNode {
private children: FileSystemNode[] = [];
constructor(public name: string) {}
add(node: FileSystemNode) { this.children.push(node); }
getSize(): number {
return this.children.reduce((sum, c) => sum + c.getSize(), 0);
}
print(indent = ""): string {
const lines = [`${indent}📁 ${this.name} (${this.getSize()}KB)`];
for (const child of this.children) {
lines.push(child.print(indent + " "));
}
return lines.join("\n");
}
}
const root = new Directory("src");
const components = new Directory("components");
components.add(new File("Button.tsx", 3));
components.add(new File("Modal.tsx", 5));
root.add(components);
root.add(new File("index.ts", 1));
console.log(root.print());
console.log("Total:", root.getSize(), "KB");Real-World Example
File systems are the quintessential Composite example—files and folders share common operations (rename, copy, get size), but folders contain other files and folders. UI component trees work the same way: a panel can contain buttons, labels, and other panels. React's component model is fundamentally a Composite tree where components can contain other components.
✓ When to Use
- •You want to represent part-whole hierarchies of objects
- •You want clients to treat all objects in the composite structure uniformly
- •You need to build recursive tree structures
- •You want to ignore the difference between compositions and individual objects
✗ When Not to Use
- •The tree structure has many different types of nodes with radically different interfaces
- •You need to enforce constraints on which components can be children of which composites
- •The uniform interface forces you to add meaningless methods to leaf classes
Understanding the Composite Pattern
The Composite pattern is one of the most important structural design patterns in object-oriented software design. Originally cataloged by the Gang of Four (Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides) in their seminal 1994 book "Design Patterns: Elements of Reusable Object-Oriented Software," this pattern continues to be a cornerstone of modern software architecture.
Composes objects into tree structures to represent part-whole hierarchies. Composite lets clients treat individual objects and compositions of objects uniformly through a shared interface, making recursive tree structures easy to build and traverse. The pattern involves the following key participants: Component, Leaf, Composite, Client. Each participant has a well-defined role, and the interactions between them create a flexible, maintainable structure that can evolve with changing requirements.
In TypeScript and modern JavaScript development, the Composite pattern is particularly valuable because the language's type system and interface support allow you to express the pattern with compile-time safety. TypeScript generics can make Composite implementations even more flexible and reusable while maintaining strong type checking throughout the pattern's structure.
Knowing when to apply the Composite pattern is as important as knowing how. Common use cases include: You want to represent part-whole hierarchies of objects; You want clients to treat all objects in the composite structure uniformly; You need to build recursive tree structures. However, overusing design patterns can lead to unnecessarily complex code. Always evaluate whether the pattern genuinely solves your problem or if a simpler approach would suffice.
The Composite pattern is related to several other design patterns: Decorator, Iterator, Visitor, Flyweight. Understanding these relationships helps you choose the right pattern for your specific situation and combine patterns effectively when building complex systems.
Related Patterns
Attaches additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing …
Provides a way to access the elements of an aggregate object sequentially without exposing its underlying representation…
Represents an operation to be performed on elements of an object structure. Visitor lets you define new operations witho…
Uses sharing to support large numbers of fine-grained objects efficiently. Flyweight reduces memory usage by sharing as …
More Structural Patterns
Explore All Design Patterns
Browse our complete reference of 23 design patterns with TypeScript examples, UML participants, and practical guidance.