Visitor

Separate an algorithm from the object structure it operates on. Add new operations to existing class hierarchies without modifying those classes — the visitor "visits" each element and does its work.

The Problem

You have a well-established AST (Abstract Syntax Tree) with node types: NumberLiteral, BinaryExpression, Identifier, FunctionCall. Now you need to: (1) pretty-print the AST, (2) evaluate it, (3) generate code, (4) type-check it. Each operation needs to handle every node type differently.

Adding each operation as methods on the nodes mixes concerns and violates Single Responsibility. Adding the fourth operation means editing every node class again. You want to add new operations without touching the node classes at all.


The Solution

The Visitor pattern uses double dispatch. Each element in the structure accepts a Visitor. The element calls visitor.visit(this) — passing itself. The visitor’s overloaded visit() method then handles that specific element type. New operations = new Visitor classes. No changes to the element hierarchy.

«interface» Visitor + visitCircle(c: Circle) + visitSquare(s: Square) «interface» Element + accept(v: Visitor) AreaCalculator + visitCircle() + visitSquare() Circle + accept(v) Square + accept(v) double dispatch
Visitor — double dispatch: element calls visitor.visitX(this); new operations = new Visitor classes

Real-World Analogy

An insurance agent (visitor) visits different types of buildings (elements): houses, apartments, office blocks. The agent fills out different forms for each building type. When a new building type is added, you create a new form template. When a new type of insurance audit is needed, you create a new agent type — you don’t modify the buildings themselves.


TypeScript Example

// Visitor interface — one method per element type
interface ASTVisitor<T> {
  visitNumber(node: NumberLiteral): T;
  visitBinary(node: BinaryExpression): T;
  visitUnary(node: UnaryExpression): T;
  visitIdentifier(node: Identifier): T;
}

// Element interface
interface ASTNode {
  accept<T>(visitor: ASTVisitor<T>): T;
}

// Concrete elements — each calls the right visitor method
class NumberLiteral implements ASTNode {
  constructor(readonly value: number) {}

  accept<T>(visitor: ASTVisitor<T>): T {
    return visitor.visitNumber(this);
  }
}

class BinaryExpression implements ASTNode {
  constructor(
    readonly left: ASTNode,
    readonly operator: '+' | '-' | '*' | '/',
    readonly right: ASTNode,
  ) {}

  accept<T>(visitor: ASTVisitor<T>): T {
    return visitor.visitBinary(this);
  }
}

class UnaryExpression implements ASTNode {
  constructor(readonly operator: '-', readonly operand: ASTNode) {}

  accept<T>(visitor: ASTVisitor<T>): T {
    return visitor.visitUnary(this);
  }
}

class Identifier implements ASTNode {
  constructor(readonly name: string) {}

  accept<T>(visitor: ASTVisitor<T>): T {
    return visitor.visitIdentifier(this);
  }
}

// Visitor 1: Evaluate the expression
class EvaluatorVisitor implements ASTVisitor<number> {
  constructor(private readonly env: Record<string, number> = {}) {}

  visitNumber(node: NumberLiteral): number { return node.value; }

  visitBinary(node: BinaryExpression): number {
    const left  = node.left.accept(this);
    const right = node.right.accept(this);
    switch (node.operator) {
      case '+': return left + right;
      case '-': return left - right;
      case '*': return left * right;
      case '/': return left / right;
    }
  }

  visitUnary(node: UnaryExpression): number {
    return -node.operand.accept(this);
  }

  visitIdentifier(node: Identifier): number {
    const val = this.env[node.name];
    if (val === undefined) throw new Error(`Undefined variable: ${node.name}`);
    return val;
  }
}

// Visitor 2: Pretty-print the expression
class PrettyPrinterVisitor implements ASTVisitor<string> {
  visitNumber(node: NumberLiteral): string {
    return String(node.value);
  }

  visitBinary(node: BinaryExpression): string {
    const left  = node.left.accept(this);
    const right = node.right.accept(this);
    return `(${left} ${node.operator} ${right})`;
  }

  visitUnary(node: UnaryExpression): string {
    return `(-${node.operand.accept(this)})`;
  }

  visitIdentifier(node: Identifier): string {
    return node.name;
  }
}

// Visitor 3: Collect all identifiers (variable names)
class IdentifierCollector implements ASTVisitor<string[]> {
  visitNumber(): string[] { return []; }

  visitBinary(node: BinaryExpression): string[] {
    return [...node.left.accept(this), ...node.right.accept(this)];
  }

  visitUnary(node: UnaryExpression): string[] {
    return node.operand.accept(this);
  }

  visitIdentifier(node: Identifier): string[] {
    return [node.name];
  }
}

// Build AST: (x + 2) * -(y - 1)
const ast = new BinaryExpression(
  new BinaryExpression(new Identifier('x'), '+', new NumberLiteral(2)),
  '*',
  new UnaryExpression('-', new BinaryExpression(new Identifier('y'), '-', new NumberLiteral(1))),
);

const printer   = new PrettyPrinterVisitor();
const evaluator = new EvaluatorVisitor({ x: 3, y: 5 });
const collector = new IdentifierCollector();

console.log('Expression:', ast.accept(printer));
// → ((x + 2) * (-(y - 1)))

console.log('Value:', ast.accept(evaluator));
// → (3+2) * -(5-1) = 5 * -4 = -20

console.log('Variables:', ast.accept(collector));
// → ['x', 'y']

When to Use

  • You need to perform many distinct, unrelated operations on an object structure without polluting those classes
  • The object structure rarely changes, but you frequently add new operations
  • Operations need to collect state across the whole structure (e.g. count nodes, generate metrics)
  • Examples: AST processing (compilers, linters), XML/JSON document traversal, UI component rendering, file system scans

Pros

  • Open/Closed — add new operations (visitors) without modifying element classes
  • Single Responsibility — each visitor focuses on one operation
  • Visitors can accumulate state across an entire object structure in one pass

Cons

  • Closed to new element types — adding a new element class forces changes to every visitor
  • Can break encapsulation — visitors often need access to element internals
  • Double dispatch is non-obvious to readers unfamiliar with the pattern

  • Composite object structures are the most common target for Visitor
  • Iterator can traverse a structure; Visitor applies an operation at each node
  • Command encapsulates a single request; Visitor encapsulates a family of operations applied to a structure