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.

«interface» Component + operation() Leaf + operation() Composite - children: Component[] + add(c: Component) 0..*
Composite — Leaf and Composite share Component interface; Composite holds 0..* children

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

  • 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