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.
- composite.ts
- Usage
/**
* 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 }];
}
import { useState, useCallback } from "react";
import { createInitialTree } from "@/data/initial-tree";
import { computeSize, countFiles, countFolders, traceFold } from "@/lib/composite";
import { addFileToTree } from "@/lib/tree-ops";
export function useFileExplorer() {
const [tree, setTree] = useState(createInitialTree);
const [highlightedNode, setHighlightedNode] = useState<string | null>(null);
const [showAddModal, setShowAddModal] = useState(false);
const [modalClosing, setModalClosing] = useState(false);
const [mobileTab, setMobileTab] = useState<"explorer" | "fold">("explorer");
const totalSize = computeSize(tree);
const fileCount = countFiles(tree);
const folderCount = countFolders(tree);
const foldSteps = traceFold(tree);
const handleAddFile = useCallback((folderPath: string[], name: string, size: number): boolean => {
const result = addFileToTree(tree, folderPath.slice(1), { name, size });
if (!result) return false;
setTree(result);
setHighlightedNode(name);
setTimeout(() => setHighlightedNode(null), 1500);
return true;
}, [tree]);
const handleReset = useCallback(() => {
setTree(createInitialTree());
setHighlightedNode(null);
}, []);
const closeModal = useCallback(() => {
setModalClosing(true);
setTimeout(() => { setShowAddModal(false); setModalClosing(false); }, 200);
}, []);
return {
tree, highlightedNode, totalSize, fileCount, folderCount, foldSteps,
showAddModal, setShowAddModal, modalClosing, closeModal,
mobileTab, setMobileTab,
handleAddFile, handleReset,
};
}
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.