Prerequisites
- Basic understanding of JavaScript 📝
- TypeScript installation ⚡
- VS Code or preferred IDE 💻
What you'll learn
- Understand the concept fundamentals 🎯
- Apply the concept in real projects 🏗️
- Debug common issues 🐛
- Write type-safe code ✨
🎯 Introduction
Have you ever looked at a file system on your computer and wondered how folders can contain both files AND other folders? 🤔 That’s the Composite Pattern in action! It’s one of the most elegant design patterns that lets you build tree-like structures where individual objects and compositions of objects are treated uniformly.
In this tutorial, we’ll explore how the Composite Pattern helps you create hierarchical structures in TypeScript. Whether you’re building a file system, a menu system, or an organizational chart, this pattern will become your best friend! Let’s dive in! 🏊♂️
📚 Understanding Composite Pattern
The Composite Pattern is like a Russian nesting doll 🪆 - you can have dolls inside dolls, and each doll can be treated the same way whether it contains other dolls or not!
What Makes It Special? 🌟
- Uniform Treatment: Treat individual objects and compositions identically
- Tree Structures: Perfect for representing hierarchies
- Recursive Composition: Components can contain other components
- Simplified Client Code: No need to check if you’re dealing with a leaf or a branch
Think of it like a company organization chart 🏢:
- A CEO manages VPs
- VPs manage Directors
- Directors manage Managers
- Managers manage Individual Contributors
Each person in the hierarchy can be treated as an “employee” regardless of whether they manage others or not!
🔧 Basic Syntax and Usage
Let’s start with the core structure of the Composite Pattern:
// 🎯 Component interface - the common interface for all objects
interface Component {
name: string;
operation(): void;
}
// 🍃 Leaf - represents end objects with no children
class Leaf implements Component {
constructor(public name: string) {}
operation(): void {
console.log(`🍃 Leaf ${this.name} is doing its thing!`);
}
}
// 📁 Composite - can contain other components
class Composite implements Component {
private children: Component[] = [];
constructor(public name: string) {}
add(component: Component): void {
this.children.push(component);
}
remove(component: Component): void {
const index = this.children.indexOf(component);
if (index !== -1) {
this.children.splice(index, 1);
}
}
operation(): void {
console.log(`📁 Composite ${this.name} is operating...`);
// 🔄 Recursively call operation on all children
this.children.forEach(child => child.operation());
}
}
Try it yourself! 🎮 Create a simple tree structure and see how both leaves and composites respond to the same operation()
call.
💡 Practical Examples
Example 1: File System Manager 📂
Let’s build a file system where folders can contain files and other folders:
// 🎯 FileSystemComponent interface
interface FileSystemComponent {
name: string;
size: number;
display(indent: string): void;
getSize(): number;
}
// 📄 File class - the leaf
class File implements FileSystemComponent {
constructor(
public name: string,
public size: number,
public emoji: string = '📄'
) {}
display(indent: string): void {
console.log(`${indent}${this.emoji} ${this.name} (${this.size}KB)`);
}
getSize(): number {
return this.size;
}
}
// 📁 Folder class - the composite
class Folder implements FileSystemComponent {
private children: FileSystemComponent[] = [];
public size: number = 0;
constructor(
public name: string,
public emoji: string = '📁'
) {}
add(component: FileSystemComponent): void {
this.children.push(component);
}
display(indent: string = ''): void {
console.log(`${indent}${this.emoji} ${this.name}/`);
// 🎨 Display all children with increased indentation
this.children.forEach(child => {
child.display(indent + ' ');
});
}
getSize(): number {
// 📊 Calculate total size recursively
return this.children.reduce((total, child) => total + child.getSize(), 0);
}
}
// 🚀 Let's build a file system!
const root = new Folder('My Documents', '💾');
const photos = new Folder('Photos', '📸');
const videos = new Folder('Videos', '🎥');
// 🖼️ Add some photos
photos.add(new File('vacation.jpg', 2048, '🏖️'));
photos.add(new File('family.jpg', 1536, '👨👩👧👦'));
photos.add(new File('sunset.jpg', 1024, '🌅'));
// 🎬 Add some videos
videos.add(new File('birthday.mp4', 51200, '🎂'));
videos.add(new File('wedding.mp4', 102400, '💑'));
// 📄 Add some documents
root.add(new File('resume.pdf', 256, '📋'));
root.add(new File('cover-letter.docx', 128, '✉️'));
root.add(photos);
root.add(videos);
// 🎯 Display the entire structure
root.display();
console.log(`\n📊 Total size: ${root.getSize()}KB`);
Example 2: Menu System 🍽️
Create a restaurant menu with categories and items:
// 🍴 MenuItem interface
interface MenuItem {
name: string;
price: number;
display(level: number): void;
getPrice(): number;
}
// 🥘 Dish - individual menu item
class Dish implements MenuItem {
constructor(
public name: string,
public price: number,
public emoji: string,
public description: string
) {}
display(level: number = 0): void {
const indent = ' '.repeat(level);
console.log(`${indent}${this.emoji} ${this.name} - $${this.price}`);
console.log(`${indent} ${this.description}`);
}
getPrice(): number {
return this.price;
}
}
// 📋 MenuCategory - can contain dishes or other categories
class MenuCategory implements MenuItem {
private items: MenuItem[] = [];
public price: number = 0;
constructor(
public name: string,
public emoji: string
) {}
add(item: MenuItem): void {
this.items.push(item);
}
display(level: number = 0): void {
const indent = ' '.repeat(level);
console.log(`\n${indent}${this.emoji} === ${this.name.toUpperCase()} ===`);
this.items.forEach(item => item.display(level + 1));
}
getPrice(): number {
// 💰 Sum of all items in category
return this.items.reduce((total, item) => total + item.getPrice(), 0);
}
}
// 🍽️ Build our menu!
const menu = new MenuCategory('Full Menu', '🍽️');
const appetizers = new MenuCategory('Appetizers', '🥗');
const mainCourses = new MenuCategory('Main Courses', '🍖');
const desserts = new MenuCategory('Desserts', '🍰');
// 🥗 Add appetizers
appetizers.add(new Dish('Caesar Salad', 8.99, '🥗', 'Fresh romaine with parmesan'));
appetizers.add(new Dish('Bruschetta', 7.99, '🥖', 'Toasted bread with tomatoes'));
appetizers.add(new Dish('Soup of the Day', 6.99, '🍲', 'Ask your server!'));
// 🍖 Add main courses
mainCourses.add(new Dish('Grilled Salmon', 24.99, '🐟', 'With lemon butter sauce'));
mainCourses.add(new Dish('Ribeye Steak', 32.99, '🥩', 'Premium aged beef'));
mainCourses.add(new Dish('Vegetable Pasta', 18.99, '🍝', 'Fresh seasonal vegetables'));
// 🍰 Add desserts
desserts.add(new Dish('Chocolate Cake', 8.99, '🍫', 'Rich dark chocolate'));
desserts.add(new Dish('Ice Cream', 5.99, '🍨', 'Vanilla, chocolate, or strawberry'));
desserts.add(new Dish('Fruit Tart', 7.99, '🥧', 'Seasonal fresh fruits'));
// 🎯 Assemble the menu
menu.add(appetizers);
menu.add(mainCourses);
menu.add(desserts);
// 📋 Display the full menu
menu.display();
console.log(`\n💰 Full menu value: $${menu.getPrice().toFixed(2)}`);
Example 3: Organization Chart 👥
Build a company hierarchy:
// 👤 Employee interface
interface Employee {
name: string;
title: string;
salary: number;
showDetails(indent: string): void;
getTotalSalary(): number;
}
// 💼 Individual Contributor
class IndividualContributor implements Employee {
constructor(
public name: string,
public title: string,
public salary: number,
public emoji: string = '👤'
) {}
showDetails(indent: string = ''): void {
console.log(`${indent}${this.emoji} ${this.name} - ${this.title} ($${this.salary.toLocaleString()})`);
}
getTotalSalary(): number {
return this.salary;
}
}
// 👔 Manager - can have reports
class Manager implements Employee {
private reports: Employee[] = [];
constructor(
public name: string,
public title: string,
public salary: number,
public emoji: string = '👔'
) {}
addReport(employee: Employee): void {
this.reports.push(employee);
console.log(`✅ ${employee.name} now reports to ${this.name}`);
}
showDetails(indent: string = ''): void {
console.log(`${indent}${this.emoji} ${this.name} - ${this.title} ($${this.salary.toLocaleString()})`);
if (this.reports.length > 0) {
console.log(`${indent}📊 Direct Reports:`);
this.reports.forEach(report => {
report.showDetails(indent + ' ');
});
}
}
getTotalSalary(): number {
// 💰 Manager's salary + all reports' salaries
const reportsSalary = this.reports.reduce(
(total, report) => total + report.getTotalSalary(),
0
);
return this.salary + reportsSalary;
}
}
// 🏢 Build the org chart!
const ceo = new Manager('Sarah Johnson', 'CEO', 500000, '👑');
const cto = new Manager('Mike Chen', 'CTO', 300000, '🔧');
const vp_eng = new Manager('Lisa Park', 'VP Engineering', 250000, '⚡');
// 👥 Engineering team
const senior_dev1 = new IndividualContributor('John Doe', 'Senior Developer', 150000, '💻');
const senior_dev2 = new IndividualContributor('Jane Smith', 'Senior Developer', 145000, '💻');
const junior_dev = new IndividualContributor('Bob Wilson', 'Junior Developer', 90000, '🌱');
// 🔗 Build the hierarchy
vp_eng.addReport(senior_dev1);
vp_eng.addReport(senior_dev2);
vp_eng.addReport(junior_dev);
cto.addReport(vp_eng);
ceo.addReport(cto);
// 📊 Show the org chart
console.log('\n🏢 Company Organization Chart:\n');
ceo.showDetails();
console.log(`\n💵 Total Salary Budget: $${ceo.getTotalSalary().toLocaleString()}`);
🚀 Advanced Concepts
Visitor Pattern Integration 🎭
Combine Composite with Visitor pattern for powerful operations:
// 🎯 Visitor interface for different operations
interface ComponentVisitor<T> {
visitFile(file: AdvancedFile): T;
visitFolder(folder: AdvancedFolder): T;
}
// 📄 Enhanced file with accept method
class AdvancedFile {
constructor(
public name: string,
public size: number,
public type: string
) {}
accept<T>(visitor: ComponentVisitor<T>): T {
return visitor.visitFile(this);
}
}
// 📁 Enhanced folder with accept method
class AdvancedFolder {
private children: (AdvancedFile | AdvancedFolder)[] = [];
constructor(public name: string) {}
add(child: AdvancedFile | AdvancedFolder): void {
this.children.push(child);
}
accept<T>(visitor: ComponentVisitor<T>): T {
return visitor.visitFolder(this);
}
getChildren(): (AdvancedFile | AdvancedFolder)[] {
return this.children;
}
}
// 🔍 Search visitor - finds files by extension
class SearchVisitor implements ComponentVisitor<string[]> {
constructor(private extension: string) {}
visitFile(file: AdvancedFile): string[] {
if (file.name.endsWith(this.extension)) {
return [file.name];
}
return [];
}
visitFolder(folder: AdvancedFolder): string[] {
const results: string[] = [];
folder.getChildren().forEach(child => {
results.push(...child.accept(this));
});
return results;
}
}
// 📊 Size calculator visitor
class SizeCalculatorVisitor implements ComponentVisitor<number> {
visitFile(file: AdvancedFile): number {
return file.size;
}
visitFolder(folder: AdvancedFolder): number {
return folder.getChildren().reduce(
(total, child) => total + child.accept(this),
0
);
}
}
// 🎨 Usage example
const root = new AdvancedFolder('src');
const components = new AdvancedFolder('components');
const utils = new AdvancedFolder('utils');
components.add(new AdvancedFile('Button.tsx', 1024, 'typescript'));
components.add(new AdvancedFile('Modal.tsx', 2048, 'typescript'));
components.add(new AdvancedFile('styles.css', 512, 'css'));
utils.add(new AdvancedFile('helpers.ts', 768, 'typescript'));
utils.add(new AdvancedFile('config.json', 256, 'json'));
root.add(components);
root.add(utils);
// 🔍 Find all TypeScript files
const searchVisitor = new SearchVisitor('.tsx');
const tsxFiles = root.accept(searchVisitor);
console.log('📘 TypeScript files:', tsxFiles);
// 📊 Calculate total size
const sizeVisitor = new SizeCalculatorVisitor();
const totalSize = root.accept(sizeVisitor);
console.log(`💾 Total size: ${totalSize} bytes`);
Generic Composite Implementation 🧬
Create a reusable generic composite:
// 🎯 Generic composite base
abstract class GenericComponent<T> {
protected children: GenericComponent<T>[] = [];
constructor(public data: T) {}
add(child: GenericComponent<T>): void {
this.children.push(child);
}
remove(child: GenericComponent<T>): void {
const index = this.children.indexOf(child);
if (index !== -1) {
this.children.splice(index, 1);
}
}
abstract operation(): void;
// 🔄 Generic traversal method
traverse(callback: (component: GenericComponent<T>) => void): void {
callback(this);
this.children.forEach(child => child.traverse(callback));
}
}
// 🎮 Game scene graph example
interface SceneNodeData {
name: string;
position: { x: number; y: number };
visible: boolean;
emoji: string;
}
class SceneNode extends GenericComponent<SceneNodeData> {
operation(): void {
if (this.data.visible) {
console.log(
`${this.data.emoji} ${this.data.name} at (${this.data.position.x}, ${this.data.position.y})`
);
}
}
}
// 🏗️ Build a game scene
const scene = new SceneNode({
name: 'Main Scene',
position: { x: 0, y: 0 },
visible: true,
emoji: '🎬'
});
const player = new SceneNode({
name: 'Player',
position: { x: 100, y: 100 },
visible: true,
emoji: '🦸'
});
const enemies = new SceneNode({
name: 'Enemies Group',
position: { x: 0, y: 0 },
visible: true,
emoji: '👾'
});
const enemy1 = new SceneNode({
name: 'Goblin',
position: { x: 200, y: 150 },
visible: true,
emoji: '👹'
});
const enemy2 = new SceneNode({
name: 'Dragon',
position: { x: 300, y: 200 },
visible: true,
emoji: '🐉'
});
// 🔗 Build scene hierarchy
enemies.add(enemy1);
enemies.add(enemy2);
scene.add(player);
scene.add(enemies);
// 🎯 Render the scene
console.log('🎮 Game Scene:');
scene.operation();
// 🔍 Find all visible entities
console.log('\n👁️ Visible Entities:');
scene.traverse(node => {
if (node.data.visible && node !== scene) {
console.log(` ${node.data.emoji} ${node.data.name}`);
}
});
⚠️ Common Pitfalls and Solutions
Pitfall 1: Circular References 🔄
❌ Wrong approach:
class BadFolder {
private parent?: BadFolder;
private children: BadFolder[] = [];
add(child: BadFolder): void {
this.children.push(child);
child.parent = this; // 🚨 Circular reference!
}
}
✅ Better approach:
class GoodFolder {
private children: GoodFolder[] = [];
private parentRef?: WeakRef<GoodFolder>; // 🛡️ Use WeakRef
add(child: GoodFolder): void {
this.children.push(child);
child.parentRef = new WeakRef(this);
}
getParent(): GoodFolder | undefined {
return this.parentRef?.deref();
}
}
Pitfall 2: Type Safety Issues 🛡️
❌ Wrong approach:
class UnsafeComposite {
private children: any[] = []; // 🚨 Using any!
add(child: any): void {
this.children.push(child);
}
}
✅ Better approach:
interface Component {
name: string;
operation(): void;
}
class SafeComposite implements Component {
private children: Component[] = []; // ✨ Type-safe!
constructor(public name: string) {}
add(child: Component): void {
this.children.push(child);
}
operation(): void {
this.children.forEach(child => child.operation());
}
}
Pitfall 3: Inefficient Traversal 🐌
❌ Wrong approach:
class InefficientFolder {
findFile(name: string): File | null {
// 🚨 Creates array on every call
const allFiles = this.getAllFiles();
return allFiles.find(f => f.name === name) || null;
}
private getAllFiles(): File[] {
// 🚨 Inefficient for deep structures
return this.children.flatMap(child =>
child instanceof Folder ? child.getAllFiles() : [child]
);
}
}
✅ Better approach:
class EfficientFolder {
findFile(name: string): File | null {
// 🚀 Early return on match
for (const child of this.children) {
if (child instanceof File && child.name === name) {
return child;
} else if (child instanceof EfficientFolder) {
const found = child.findFile(name);
if (found) return found;
}
}
return null;
}
}
🛠️ Best Practices
-
🎯 Define Clear Component Interface
- Keep the interface minimal and focused
- Include only operations common to all components
-
🔒 Maintain Type Safety
- Use TypeScript interfaces and generics
- Avoid
any
type at all costs
-
🌳 Consider Tree Depth
- Implement depth limits for deep hierarchies
- Use iterative approaches for very large trees
-
💾 Memory Management
- Use WeakRef for parent references
- Implement proper cleanup methods
-
🚀 Performance Optimization
- Cache computed values when appropriate
- Use lazy loading for large structures
🧪 Hands-On Exercise
Challenge: Build a Task Management System! 📋
Create a task management system where:
- Tasks can be individual items or task groups
- Each task has a status (pending ⏳, in-progress 🔄, completed ✅)
- Task groups show aggregate status
- Calculate completion percentage
Requirements:
- Use the Composite Pattern
- Include emoji status indicators
- Support nested task groups
- Show progress for each level
💡 Click here for the solution
// 🎯 Task interface
interface Task {
name: string;
getStatus(): string;
getProgress(): number;
display(indent: string): void;
}
// 📝 Individual task
class SimpleTask implements Task {
private status: 'pending' | 'in-progress' | 'completed' = 'pending';
constructor(public name: string) {}
setStatus(status: 'pending' | 'in-progress' | 'completed'): void {
this.status = status;
}
getStatus(): string {
const statusEmojis = {
'pending': '⏳',
'in-progress': '🔄',
'completed': '✅'
};
return statusEmojis[this.status];
}
getProgress(): number {
return this.status === 'completed' ? 100 :
this.status === 'in-progress' ? 50 : 0;
}
display(indent: string = ''): void {
console.log(`${indent}${this.getStatus()} ${this.name}`);
}
}
// 📁 Task group
class TaskGroup implements Task {
private tasks: Task[] = [];
constructor(public name: string) {}
add(task: Task): void {
this.tasks.push(task);
}
getStatus(): string {
const progress = this.getProgress();
if (progress === 100) return '✅';
if (progress > 0) return '🔄';
return '⏳';
}
getProgress(): number {
if (this.tasks.length === 0) return 0;
const totalProgress = this.tasks.reduce(
(sum, task) => sum + task.getProgress(),
0
);
return Math.round(totalProgress / this.tasks.length);
}
display(indent: string = ''): void {
console.log(`${indent}${this.getStatus()} ${this.name} [${this.getProgress()}%]`);
this.tasks.forEach(task => task.display(indent + ' '));
}
}
// 🚀 Create project structure
const project = new TaskGroup('🚀 Launch Website Project');
// Frontend tasks
const frontend = new TaskGroup('🎨 Frontend Development');
const design = new SimpleTask('Create mockups');
const html = new SimpleTask('Build HTML structure');
const css = new SimpleTask('Style with CSS');
const js = new SimpleTask('Add interactivity');
design.setStatus('completed');
html.setStatus('completed');
css.setStatus('in-progress');
frontend.add(design);
frontend.add(html);
frontend.add(css);
frontend.add(js);
// Backend tasks
const backend = new TaskGroup('⚙️ Backend Development');
const api = new SimpleTask('Design API');
const database = new SimpleTask('Set up database');
const auth = new SimpleTask('Implement authentication');
api.setStatus('completed');
database.setStatus('in-progress');
backend.add(api);
backend.add(database);
backend.add(auth);
// Testing
const testing = new TaskGroup('🧪 Testing');
const unit = new SimpleTask('Write unit tests');
const integration = new SimpleTask('Integration tests');
const e2e = new SimpleTask('End-to-end tests');
testing.add(unit);
testing.add(integration);
testing.add(e2e);
// Build the project
project.add(frontend);
project.add(backend);
project.add(testing);
// Display the project status
console.log('📋 Project Status:\n');
project.display();
console.log(`\n🎯 Overall Progress: ${project.getProgress()}%`);
🎓 Key Takeaways
You’ve just mastered the Composite Pattern! Here’s what you’ve learned:
- 🌳 Tree Structures: Build hierarchical systems with ease
- 🎯 Uniform Interface: Treat leaves and composites the same way
- 🔄 Recursive Power: Operations propagate through the entire tree
- 💪 Real-World Applications: File systems, menus, org charts, and more!
The Composite Pattern is your go-to solution whenever you need to represent part-whole hierarchies. Whether you’re building a UI component tree, a document structure, or any hierarchical system, this pattern makes your code cleaner and more maintainable!
🤝 Next Steps
Now that you’ve mastered the Composite Pattern, here’s what to explore next:
- 🎭 Combine with Visitor Pattern - Add new operations without modifying classes
- 🔄 Iterator Pattern - Traverse your composite structures efficiently
- 🏗️ Builder Pattern - Create complex composite structures step by step
- 🎯 Command Pattern - Execute operations on your composite structures
- 📦 Decorator Pattern - Add responsibilities to your components dynamically
Keep building those tree structures, and remember - with the Composite Pattern, you’re not just writing code, you’re growing forests of functionality! 🌲✨
Happy coding! 🎉