Skip to article
ALGORITHMICSPatterns
Patterns6 min read

Visitor

Adding an operation without editing the classes — and the price it charges for that.


An expression tree: numbers, additions, multiplications. You need to evaluate it. Then print it. Then measure its depth. Then type-check it. Then optimise it.

Each new operation means a new method on every node class. Five operations across three node types is fifteen methods, and the node classes keep changing for reasons that have nothing to do with what a node is.

The pattern

Move the operation into its own object, and let nodes hand themselves to it.

interface Visitor<T> {
visitNum(node: Num): T;
visitAdd(node: Add): T;
visitMul(node: Mul): T;
}
abstract class Node {
abstract accept<T>(visitor: Visitor<T>): T;
}
class Add extends Node {
constructor(readonly left: Node, readonly right: Node) { super(); }
accept<T>(visitor: Visitor<T>): T { return visitor.visitAdd(this); }
}

An operation is now one class:

class Evaluate implements Visitor<number> {
visitNum(n: Num) { return n.value; }
visitAdd(n: Add) { return n.left.accept(this) + n.right.accept(this); }
visitMul(n: Mul) { return n.left.accept(this) * n.right.accept(this); }
}

Three passes over the same tree, none of which touched a node class:

Visitor
Add
├─ Num 2
└─ Mul
   ├─ Num 3
   └─ Num 4

14

A number. The visitor combines children with the node’s own operator.

The trade, stated plainly

Easy: a new operation. One class, zero edits elsewhere.

Hard: a new node type. visitDivide goes on the Visitor interface, and every existing visitor must implement it.

In TypeScript, use a discriminated union

If the node types are yours and finite, the language has a better answer:

type Node =
| {kind: 'num'; value: number}
| {kind: 'add'; left: Node; right: Node}
| {kind: 'mul'; left: Node; right: Node};
function evaluate(node: Node): number {
switch (node.kind) {
case 'num': return node.value;
case 'add': return evaluate(node.left) + evaluate(node.right);
case 'mul': return evaluate(node.left) * evaluate(node.right);
}
}

An operation is one function. Adding a node type makes the compiler flag every switch that no longer covers all cases — the same protection the visitor interface gave, without accept methods.

This is Visitor’s benefit with none of its ceremony, and it is what you should write when the type set is closed and under your control. Reach for the classic version when node types come from elsewhere, or when the language has no sum types.

What it costs

Encapsulation. Visitors need the node’s data, so fields go public. The operation has been separated from the type by exposing the type’s insides.

Traversal is duplicated. Every visitor above recurses into children itself. A shared base visitor helps, but then visitors that need a different order have to fight it.

It is a lot of code. For three node types and two operations, do not do this. The break-even is somewhere around five of each.

Where it already is

Compilers, everywhere — an AST plus one visitor per pass. ESLint rules are visitors over the JavaScript AST. Babel plugins are visitors. XML/DOM traversal APIs. Anywhere the data shape is fixed and the number of things you do to it is not.