Skip to main content

Composite Pattern

Compose objects into tree structures to represent part-whole hierarchies. Treat individual objects and compositions uniformly.


The Problem​

You're building a file explorer. Files have a size. Folders contain files and other folders. You need to display the total size of any folder, calculated recursively from its contents.

The naive approach:

function getSize(item: File | Folder): number {
if (item.type === "file") {
return item.size;
}
let total = 0;
for (const child of item.children) {
if (child.type === "file") {
total += child.size;
} else {
total += getSize(child); // recursive, but type checks everywhere
}
}
return total;
}

Type checks at every level. Adding a new operation (count files, find largest, render tree) means writing another recursive function with the same if/else structure.


The Solution​

Model the tree as a discriminated union. Use fold to traverse it uniformly:

import { leaf, branch, fold } from "@pithos/core/eidos/composite/composite";

const project = branch({ name: "project", size: 0 }, [
leaf({ name: "README.md", size: 1024 }),
branch({ name: "src", size: 0 }, [
leaf({ name: "index.ts", size: 2048 }),
leaf({ name: "utils.ts", size: 512 }),
]),
branch({ name: "docs", size: 0 }, [
leaf({ name: "guide.md", size: 768 }),
]),
]);

// Total size: one fold, no type checks
const totalSize = fold(project, {
leaf: (data) => data.size,
branch: (_data, childSizes) => childSizes.reduce((a, b) => a + b, 0),
}); // 4352

// File count: same structure, different logic
const fileCount = fold(project, {
leaf: () => 1,
branch: (_data, counts) => counts.reduce((a, b) => a + b, 0),
}); // 4

One traversal pattern. Add new operations without modifying the tree. Sizes recalculate automatically when you add or remove nodes.


Live Demo​

A file explorer with foldable directories. Each file shows its size, each folder shows its total size computed recursively via fold. Add files and watch sizes recalculate up the tree. A panel shows the fold operation in action.

Code
/**
* Composite pattern operations via fold().
*
* fold() traverses the tree bottom-up: leaves emit values,
* branches reduce their children's results.
*/

import { fold } from "@pithos/core/eidos/composite/composite";
import type { FileTree, FoldStep } from "./types";

export { fold };

/** Fold a tree by summing: leaf emits a value, branch sums children. */
function sumFold(node: FileTree, leafValue: (data: FileTree["data"]) => number, branchBonus: number = 0): number {
return fold(node, {
leaf: (data) => leafValue(data),
branch: (_data, children) => branchBonus + children.reduce((a, b) => a + b, 0),
});
}

export function computeSize(node: FileTree): number {
return sumFold(node, (data) => data.size);
}

export function countFiles(node: FileTree): number {
return sumFold(node, () => 1);
}

export function countFolders(node: FileTree): number {
return sumFold(node, () => 0, 1);
}

export function traceFold(node: FileTree, depth: number = 0): FoldStep[] {
if (node.type === "leaf") {
return [{ name: node.data.name, type: "leaf", result: node.data.size, depth }];
}
const childSteps = node.children.flatMap((child) => traceFold(child, depth + 1));
const total = computeSize(node);
return [...childSteps, { name: node.data.name, type: "branch", result: total, depth }];
}
Result

Real-World Analogy​

A company org chart. Departments contain teams, teams contain people. When you ask "how many employees?", you don't care if you're asking a department, team, or individual: the question works the same way at every level.


When to Use It​

  • Represent hierarchical data (files, org charts, UI components, menus)
  • Apply operations uniformly to leaves and branches
  • Build recursive structures with type safety
  • Need sizes, counts, or aggregations that propagate up the tree

When NOT to Use It​

If your data is flat (a simple list), don't force it into a tree. Composite adds complexity that only pays off with genuinely hierarchical structures.


API​

  • leaf β€” Create a leaf node with data
  • branch β€” Create a branch node with children
  • fold β€” Reduce a tree to a single value
  • map β€” Transform all nodes in a tree
  • flatten β€” Collect all leaf values
  • find β€” Search for a node matching a predicate