Loading learning content...
The Visitor pattern solves the operation-extension problem through an elegant mechanism called double dispatch. Instead of fighting OOP's single dispatch limitation, it works with it—using two polymorphic calls to achieve what languages don't provide natively.
The result: you can add new operations to a stable object hierarchy without modifying any of its classes. Each operation lives in its own visitor class, completely decoupled from the elements it operates on.
By the end of this page, you will understand the Visitor pattern's structure inside and out. You'll master double dispatch, implement visitors from scratch, understand the role of each participant, and see how the pattern achieves type-safe polymorphism without instanceof checks.
Most OOP languages support single dispatch: method selection depends on one object's runtime type—the receiver. The Visitor pattern achieves double dispatch by combining two single dispatches.
The mechanism in two steps:
First dispatch (on element type): Client calls element.accept(visitor). The runtime selects the correct accept method based on the element's actual type.
Second dispatch (on visitor type): Inside accept, the element calls visitor.visit[ElementType](this). The runtime selects the correct visitor method, and the element passes itself with its concrete type.
Together, these two dispatches select behavior based on both the element type and the visitor type—exactly what we need.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879
// Step-by-step walkthrough of double dispatch: // Given these types:interface Visitor { visitParagraph(p: Paragraph): void; visitHeading(h: Heading): void; visitImage(i: Image): void;} interface Element { accept(visitor: Visitor): void;} class Paragraph implements Element { constructor(public text: string) {} accept(visitor: Visitor): void { // ← SECOND DISPATCH: calls specific visitor method visitor.visitParagraph(this); // "this" has type Paragraph }} class Heading implements Element { constructor(public text: string, public level: number) {} accept(visitor: Visitor): void { visitor.visitHeading(this); // "this" has type Heading }} // And a specific visitor:class HtmlRenderer implements Visitor { private output: string[] = []; visitParagraph(p: Paragraph): void { this.output.push(`<p>${p.text}</p>`); } visitHeading(h: Heading): void { this.output.push(`<h${h.level}>${h.text}</h${h.level}>`); } visitImage(i: Image): void { this.output.push(`<img src="${i.src}" alt="${i.alt}" />`); } getHtml(): string { return this.output.join('\n'); }} // USAGE: Illustrating the two dispatchesconst elements: Element[] = [ new Heading("Welcome", 1), new Paragraph("Hello, world!"), new Image("photo.jpg", "A photo"),]; const renderer = new HtmlRenderer(); for (const element of elements) { element.accept(renderer); // ↑ ↑ // │ └── Argument (visitor) - used for second dispatch // └── Receiver - used for first dispatch // // For element = Heading: // 1st dispatch: element.accept() → Heading.accept() (based on element type) // 2nd dispatch: visitor.visitHeading() → HtmlRenderer.visitHeading() // // Result: HtmlRenderer's implementation for Heading is called // WITHOUT any instanceof checks!} console.log(renderer.getHtml());// Output:// <h1>Welcome</h1>// <p>Hello, world!</p>// <img src="photo.jpg" alt="A photo" />The magic happens because each element type's accept method calls a different method on the visitor (visitParagraph, visitHeading, etc.). This converts the element's runtime type into a compile-time known method call. The visitor's polymorphism then handles the visitor type. Two dispatches—one for each type dimension.
The Visitor pattern involves several participants with distinct responsibilities. Understanding each role is crucial for correct implementation.
| Participant | Responsibility | Typical Implementation |
|---|---|---|
| Visitor (Interface) | Declares visit methods for each concrete element type | Interface with visitX(x: X) for each element type |
| Concrete Visitor | Implements specific operation for all element types | Class implementing Visitor, one per operation |
| Element (Interface) | Declares accept(visitor) method | Interface with accept(visitor: Visitor): void |
| Concrete Element | Implements accept by calling visitor's type-specific method | Class calling visitor.visitThis(this) |
| Object Structure | Holds elements, provides traversal if needed | Collection, composite tree, or graph |
| Client | Creates visitors and applies them to structure | Application code orchestrating visits |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
// ═══════════════════════════════════════════════════════════════// VISITOR INTERFACE// Declares a visit method for each concrete element type.// The method names encode the element type, enabling dispatch.// ═══════════════════════════════════════════════════════════════ interface DocumentVisitor { visitParagraph(element: Paragraph): void; visitHeading(element: Heading): void; visitImage(element: Image): void; visitTable(element: Table): void; visitCodeBlock(element: CodeBlock): void;} // ═══════════════════════════════════════════════════════════════// ELEMENT INTERFACE// Declares the accept method that takes a visitor.// This is the hook for the first dispatch.// ═══════════════════════════════════════════════════════════════ interface DocumentElement { accept(visitor: DocumentVisitor): void;} // ═══════════════════════════════════════════════════════════════// CONCRETE ELEMENTS// Each implements accept by calling visitor's type-specific method.// The element "knows" its own type and encodes it in the call.// ═══════════════════════════════════════════════════════════════ class Paragraph implements DocumentElement { constructor(public readonly text: string) {} accept(visitor: DocumentVisitor): void { // Paragraph calls visitParagraph, encoding its type visitor.visitParagraph(this); }} class Heading implements DocumentElement { constructor( public readonly text: string, public readonly level: 1 | 2 | 3 | 4 | 5 | 6 ) {} accept(visitor: DocumentVisitor): void { visitor.visitHeading(this); }} class Image implements DocumentElement { constructor( public readonly src: string, public readonly alt: string, public readonly width?: number, public readonly height?: number ) {} accept(visitor: DocumentVisitor): void { visitor.visitImage(this); }} class Table implements DocumentElement { constructor( public readonly headers: string[], public readonly rows: string[][] ) {} accept(visitor: DocumentVisitor): void { visitor.visitTable(this); }} class CodeBlock implements DocumentElement { constructor( public readonly code: string, public readonly language: string ) {} accept(visitor: DocumentVisitor): void { visitor.visitCodeBlock(this); }} // ═══════════════════════════════════════════════════════════════// OBJECT STRUCTURE// Holds elements and provides a way to iterate or traverse them.// ═══════════════════════════════════════════════════════════════ class Document { private elements: DocumentElement[] = []; addElement(element: DocumentElement): void { this.elements.push(element); } // Visitor traversal: applies visitor to each element accept(visitor: DocumentVisitor): void { for (const element of this.elements) { element.accept(visitor); } } // Alternative: Let visitors control traversal getElements(): ReadonlyArray<DocumentElement> { return this.elements; }} // ═══════════════════════════════════════════════════════════════// CONCRETE VISITORS (Operations)// Each visitor implements ONE operation for ALL element types.// Adding new operations = adding new visitor classes.// ═══════════════════════════════════════════════════════════════ class HtmlExportVisitor implements DocumentVisitor { private parts: string[] = []; visitParagraph(element: Paragraph): void { this.parts.push(`<p>${this.escapeHtml(element.text)}</p>`); } visitHeading(element: Heading): void { const tag = `h${element.level}`; this.parts.push(`<${tag}>${this.escapeHtml(element.text)}</${tag}>`); } visitImage(element: Image): void { const dimensions = element.width && element.height ? `width="${element.width}" height="${element.height}"` : ''; this.parts.push(`<img src="${element.src}" alt="${element.alt}" ${dimensions} />`); } visitTable(element: Table): void { const headerRow = element.headers.map(h => `<th>${h}</th>`).join(''); const bodyRows = element.rows.map(row => `<tr>${row.map(cell => `<td>${cell}</td>`).join('')}</tr>` ).join(''); this.parts.push(`<table><thead><tr>${headerRow}</tr></thead><tbody>${bodyRows}</tbody></table>`); } visitCodeBlock(element: CodeBlock): void { this.parts.push(`<pre><code class="language-${element.language}">${this.escapeHtml(element.code)}</code></pre>`); } getResult(): string { return this.parts.join('\n'); } private escapeHtml(text: string): string { return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); }} class WordCountVisitor implements DocumentVisitor { private wordCount = 0; visitParagraph(element: Paragraph): void { this.wordCount += this.countWords(element.text); } visitHeading(element: Heading): void { this.wordCount += this.countWords(element.text); } visitImage(element: Image): void { // Images contribute alt text to word count this.wordCount += this.countWords(element.alt); } visitTable(element: Table): void { // Count words in headers and cells for (const header of element.headers) { this.wordCount += this.countWords(header); } for (const row of element.rows) { for (const cell of row) { this.wordCount += this.countWords(cell); } } } visitCodeBlock(element: CodeBlock): void { // Code blocks might not count, or count differently // Here we count identifiers as "words" this.wordCount += this.countWords(element.code); } getResult(): number { return this.wordCount; } private countWords(text: string): number { return text.trim().split(/\s+/).filter(w => w.length > 0).length; }}Think of the pattern as a matrix:
Traditional OOP puts cells in rows (each element implements all operations). Visitor puts cells in columns (each visitor implements one operation for all elements).
The accept method is deceptively simple—often just a single line. Yet it's the cornerstone of the entire pattern. Let's understand exactly what it does and why every element must implement it.
12345678910111213141516171819202122232425262728293031323334353637383940
class Paragraph implements DocumentElement { accept(visitor: DocumentVisitor): void { visitor.visitParagraph(this); // ↑ ↑ // │ └── "this" is typed as Paragraph (concrete type) // └── Method name encodes element type }} // Why can't we implement this in a base class? abstract class BaseElement implements DocumentElement { // ❌ THIS DOESN'T WORK! accept(visitor: DocumentVisitor): void { // What method do we call? We don't know the concrete type here. // visitor.visit???(this); // We could try reflection or type tags, but that defeats the purpose. // The whole point is compile-time type safety! }} // Each concrete class MUST implement accept:// - It knows its own type// - It calls the visitor method for its type// - It passes "this" with its concrete type, not as base type class Paragraph extends BaseElement { accept(visitor: DocumentVisitor): void { // "this" here is Paragraph, not BaseElement visitor.visitParagraph(this); }} class Heading extends BaseElement { accept(visitor: DocumentVisitor): void { // "this" here is Heading visitor.visitHeading(this); }}Paragraph.accept calls visitParagraph, the type is now statically known.this with its concrete type, giving the visitor access to type-specific properties and methods.A common mistake is trying to implement accept in a base class. This cannot work because:
visit method to callvisitor.visit(this), this would have the base typeThe basic Visitor pattern uses void return types and accumulates results in the visitor. But real-world usage often needs return values. Let's explore approaches for returning results from visits.
1234567891011121314151617181920212223242526272829303132
// Visitor accumulates results internally, caller retrieves after traversal interface DocumentVisitor { visitParagraph(element: Paragraph): void; visitHeading(element: Heading): void; visitImage(element: Image): void;} class HtmlExportVisitor implements DocumentVisitor { private result = ''; visitParagraph(element: Paragraph): void { this.result += `<p>${element.text}</p>\n`; } visitHeading(element: Heading): void { this.result += `<h${element.level}>${element.text}</h${element.level}>\n`; } visitImage(element: Image): void { this.result += `<img src="${element.src}" alt="${element.alt}" />\n`; } getResult(): string { return this.result; }} // Usage:const visitor = new HtmlExportVisitor();document.accept(visitor);const html = visitor.getResult();1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465
// Visitor interface is generic over return type interface DocumentVisitor<R> { visitParagraph(element: Paragraph): R; visitHeading(element: Heading): R; visitImage(element: Image): R;} interface DocumentElement { accept<R>(visitor: DocumentVisitor<R>): R;} class Paragraph implements DocumentElement { constructor(public readonly text: string) {} accept<R>(visitor: DocumentVisitor<R>): R { return visitor.visitParagraph(this); }} class Heading implements DocumentElement { constructor(public readonly text: string, public readonly level: number) {} accept<R>(visitor: DocumentVisitor<R>): R { return visitor.visitHeading(this); }} // Visitors specify their return type: class HtmlExportVisitor implements DocumentVisitor<string> { visitParagraph(element: Paragraph): string { return `<p>${element.text}</p>`; } visitHeading(element: Heading): string { return `<h${element.level}>${element.text}</h${element.level}>`; } visitImage(element: Image): string { return `<img src="${element.src}" alt="${element.alt}" />`; }} class WordCountVisitor implements DocumentVisitor<number> { visitParagraph(element: Paragraph): number { return element.text.split(/\s+/).length; } visitHeading(element: Heading): number { return element.text.split(/\s+/).length; } visitImage(element: Image): number { return element.alt.split(/\s+/).length; }} // Usage - type-safe returns:const heading = new Heading("Welcome", 1);const htmlVisitor = new HtmlExportVisitor();const wordVisitor = new WordCountVisitor(); const html: string = heading.accept(htmlVisitor); // Type: stringconst words: number = heading.accept(wordVisitor); // Type: number12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455
// Pass context in, get results out - useful for complex transformations interface VisitorContext<TContext, TResult> { context: TContext; result?: TResult;} interface DocumentVisitor<TContext, TResult> { visitParagraph(element: Paragraph, ctx: VisitorContext<TContext, TResult>): void; visitHeading(element: Heading, ctx: VisitorContext<TContext, TResult>): void; visitImage(element: Image, ctx: VisitorContext<TContext, TResult>): void;} interface DocumentElement { accept<TContext, TResult>( visitor: DocumentVisitor<TContext, TResult>, ctx: VisitorContext<TContext, TResult> ): void;} // Useful for visitors that need configuration or accumulated state interface PdfContext { renderer: PdfRenderer; currentPage: number; y: number;} class PdfExportVisitor implements DocumentVisitor<PdfContext, PdfDocument> { visitParagraph(element: Paragraph, ctx: VisitorContext<PdfContext, PdfDocument>): void { const { renderer, currentPage, y } = ctx.context; renderer.drawText(element.text, { page: currentPage, y }); ctx.context.y += 20; // Move cursor } visitHeading(element: Heading, ctx: VisitorContext<PdfContext, PdfDocument>): void { const { renderer, currentPage, y } = ctx.context; renderer.drawText(element.text, { page: currentPage, y, fontSize: 24 - (element.level * 2) }); ctx.context.y += 30; } visitImage(element: Image, ctx: VisitorContext<PdfContext, PdfDocument>): void { // Check if we need a new page if (ctx.context.y > 700) { ctx.context.currentPage++; ctx.context.y = 50; } ctx.context.renderer.drawImage(element.src, ctx.context.y); ctx.context.y += element.height || 200; }}Use Accumulator when: Results are built incrementally (HTML string building, statistics gathering).
Use Generic Return when: Each element visit produces an independent result (transformation, validation).
Use Contextual when: Visits need shared state or configuration (rendering with position tracking, validation with schema context).
Let's build a complete, working Visitor implementation for an expression evaluator. This example demonstrates all pattern components working together.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
// ═══════════════════════════════════════════════════════════════// COMPLETE EXPRESSION VISITOR IMPLEMENTATION// Demonstrates: evaluation, pretty printing, and type checking// ═══════════════════════════════════════════════════════════════ // ─── Visitor Interface ───────────────────────────────────────── interface ExpressionVisitor<R> { visitNumber(expr: NumberExpr): R; visitBinary(expr: BinaryExpr): R; visitUnary(expr: UnaryExpr): R; visitVariable(expr: VariableExpr): R; visitCall(expr: CallExpr): R;} // ─── Element Interface ───────────────────────────────────────── interface Expression { accept<R>(visitor: ExpressionVisitor<R>): R;} // ─── Concrete Elements ───────────────────────────────────────── class NumberExpr implements Expression { constructor(public readonly value: number) {} accept<R>(visitor: ExpressionVisitor<R>): R { return visitor.visitNumber(this); }} class BinaryExpr implements Expression { constructor( public readonly left: Expression, public readonly operator: '+' | '-' | '*' | '/' | '%' | '**', public readonly right: Expression ) {} accept<R>(visitor: ExpressionVisitor<R>): R { return visitor.visitBinary(this); }} class UnaryExpr implements Expression { constructor( public readonly operator: '-' | '+' | '!', public readonly operand: Expression ) {} accept<R>(visitor: ExpressionVisitor<R>): R { return visitor.visitUnary(this); }} class VariableExpr implements Expression { constructor(public readonly name: string) {} accept<R>(visitor: ExpressionVisitor<R>): R { return visitor.visitVariable(this); }} class CallExpr implements Expression { constructor( public readonly functionName: string, public readonly args: Expression[] ) {} accept<R>(visitor: ExpressionVisitor<R>): R { return visitor.visitCall(this); }} // ─── Concrete Visitor 1: Evaluator ───────────────────────────── class EvaluateVisitor implements ExpressionVisitor<number> { constructor(private environment: Map<string, number> = new Map()) {} visitNumber(expr: NumberExpr): number { return expr.value; } visitBinary(expr: BinaryExpr): number { const left = expr.left.accept(this); const right = expr.right.accept(this); switch (expr.operator) { case '+': return left + right; case '-': return left - right; case '*': return left * right; case '/': return left / right; case '%': return left % right; case '**': return Math.pow(left, right); } } visitUnary(expr: UnaryExpr): number { const operand = expr.operand.accept(this); switch (expr.operator) { case '-': return -operand; case '+': return +operand; case '!': return operand === 0 ? 1 : 0; // Logical NOT } } visitVariable(expr: VariableExpr): number { const value = this.environment.get(expr.name); if (value === undefined) { throw new Error(`Undefined variable: ${expr.name}`); } return value; } visitCall(expr: CallExpr): number { const args = expr.args.map(arg => arg.accept(this)); switch (expr.functionName) { case 'sqrt': return Math.sqrt(args[0]); case 'abs': return Math.abs(args[0]); case 'sin': return Math.sin(args[0]); case 'cos': return Math.cos(args[0]); case 'max': return Math.max(...args); case 'min': return Math.min(...args); default: throw new Error(`Unknown function: ${expr.functionName}`); } }} // ─── Concrete Visitor 2: Pretty Printer ──────────────────────── class PrettyPrintVisitor implements ExpressionVisitor<string> { visitNumber(expr: NumberExpr): string { return expr.value.toString(); } visitBinary(expr: BinaryExpr): string { const left = expr.left.accept(this); const right = expr.right.accept(this); return `(${left} ${expr.operator} ${right})`; } visitUnary(expr: UnaryExpr): string { const operand = expr.operand.accept(this); return `(${expr.operator}${operand})`; } visitVariable(expr: VariableExpr): string { return expr.name; } visitCall(expr: CallExpr): string { const args = expr.args.map(arg => arg.accept(this)).join(', '); return `${expr.functionName}(${args})`; }} // ─── Concrete Visitor 3: Complexity Calculator ───────────────── class ComplexityVisitor implements ExpressionVisitor<number> { visitNumber(expr: NumberExpr): number { return 1; // Base complexity } visitBinary(expr: BinaryExpr): number { const leftComplexity = expr.left.accept(this); const rightComplexity = expr.right.accept(this); const opWeight = expr.operator === '**' ? 3 : 1; // Exponentiation is expensive return leftComplexity + rightComplexity + opWeight; } visitUnary(expr: UnaryExpr): number { return expr.operand.accept(this) + 1; } visitVariable(expr: VariableExpr): number { return 2; // Variable lookup has some cost } visitCall(expr: CallExpr): number { const argComplexity = expr.args.reduce( (sum, arg) => sum + arg.accept(this), 0 ); const funcWeight = expr.functionName === 'sqrt' ? 5 : 3; // sqrt is expensive return argComplexity + funcWeight; }} // ─── Usage Example ───────────────────────────────────────────── // Build expression: sqrt(x ** 2 + y ** 2)const expression = new CallExpr('sqrt', [ new BinaryExpr( new BinaryExpr(new VariableExpr('x'), '**', new NumberExpr(2)), '+', new BinaryExpr(new VariableExpr('y'), '**', new NumberExpr(2)) )]); // Apply different visitors to the SAME expression: const printer = new PrettyPrintVisitor();console.log(expression.accept(printer));// Output: sqrt(((x ** 2) + (y ** 2))) const evaluator = new EvaluateVisitor(new Map([['x', 3], ['y', 4]]));console.log(expression.accept(evaluator));// Output: 5 (sqrt(9 + 16) = sqrt(25) = 5) const complexity = new ComplexityVisitor();console.log(expression.accept(complexity));// Output: 19 (computed complexity score)When elements form trees or graphs, you need to decide who controls traversal: the elements, the visitor, or a separate iterator. Each approach has different implications.
| Strategy | Who Traverses | Pros | Cons |
|---|---|---|---|
| Element-Driven | Each element visits its children in accept() | Simple for element, visitor is passive | Visitor can't skip or reorder children |
| Visitor-Driven | Visitor explicitly visits children | Full control over traversal | Visitor must know structure |
| Iterator-Driven | External iterator/structure | Decoupled from both | Additional complexity |
| Hybrid | Elements handle structure, visitor can influence | Flexible | More complex protocol |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
// ─── Strategy 1: Element-Driven Traversal ────────────────────── // Elements automatically traverse childrenclass BinaryExpr implements Expression { constructor( public readonly left: Expression, public readonly operator: string, public readonly right: Expression ) {} accept<R>(visitor: ExpressionVisitor<R>): R { // Element controls order: first left, then right, then self this.left.accept(visitor); // Visit left subtree this.right.accept(visitor); // Visit right subtree return visitor.visitBinary(this); // Visit self }} // Pro: Visitor doesn't need to know about children// Con: Visitor can't skip children or visit in different order // ─── Strategy 2: Visitor-Driven Traversal ────────────────────── // Elements expose structure, visitor chooses whether to traverseinterface Expression { accept<R>(visitor: ExpressionVisitor<R>): R; // Visitor can access children directly if needed} class OptimizingVisitor implements ExpressionVisitor<Expression> { visitBinary(expr: BinaryExpr): Expression { // Visitor decides to evaluate children first const left = expr.left.accept(this); const right = expr.right.accept(this); // Can skip processing if both are constants if (left instanceof NumberExpr && right instanceof NumberExpr) { // Constant folding - don't traverse further return new NumberExpr(this.compute(left.value, expr.operator, right.value)); } return new BinaryExpr(left, expr.operator, right); } visitNumber(expr: NumberExpr): Expression { return expr; // Numbers don't have children } // ... other visit methods private compute(left: number, op: string, right: number): number { switch (op) { case '+': return left + right; case '*': return left * right; // ... } }} // ─── Strategy 3: External Iterator ───────────────────────────── // Separate traversal from visiting entirelyclass ExpressionIterator { *preorder(expr: Expression): Generator<Expression> { yield expr; if (expr instanceof BinaryExpr) { yield* this.preorder(expr.left); yield* this.preorder(expr.right); } else if (expr instanceof UnaryExpr) { yield* this.preorder(expr.operand); } else if (expr instanceof CallExpr) { for (const arg of expr.args) { yield* this.preorder(arg); } } } *postorder(expr: Expression): Generator<Expression> { if (expr instanceof BinaryExpr) { yield* this.postorder(expr.left); yield* this.postorder(expr.right); } else if (expr instanceof UnaryExpr) { yield* this.postorder(expr.operand); } else if (expr instanceof CallExpr) { for (const arg of expr.args) { yield* this.postorder(arg); } } yield expr; }} // Now visitor can be used with any traversalconst iterator = new ExpressionIterator();const visitor = new SomeVisitor(); for (const node of iterator.preorder(rootExpression)) { node.accept(visitor);}Element-driven is simplest—use it when traversal order is fixed and visitors don't need control.
Visitor-driven is most flexible—use it when visitors need to transform the tree, skip subtrees, or change order.
External iterator provides maximum decoupling—use it when you need multiple traversal orders or when the structure is complex (graphs, not just trees).
Implementing the Visitor pattern correctly requires attention to several details. Here are guidelines based on real-world experience.
visit + type name: visitParagraph, visitHeading. Some prefer visit + past participle for events: visitedParagraph.ExpressionVisitor<R>.12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849
// Base class with default no-op implementationsabstract class BaseDocumentVisitor implements DocumentVisitor { visitParagraph(element: Paragraph): void { /* no-op */ } visitHeading(element: Heading): void { /* no-op */ } visitImage(element: Image): void { /* no-op */ } visitTable(element: Table): void { /* no-op */ } visitCodeBlock(element: CodeBlock): void { /* no-op */ }} // Visitors only override what they care aboutclass HeadingCollector extends BaseDocumentVisitor { private headings: Heading[] = []; // Only override visitHeading - others use no-op default visitHeading(element: Heading): void { this.headings.push(element); } getHeadings(): Heading[] { return this.headings; }} // Alternative: Partial visitor with compile-time safetyinterface PartialDocumentVisitor { visitParagraph?(element: Paragraph): void; visitHeading?(element: Heading): void; visitImage?(element: Image): void; visitTable?(element: Table): void; visitCodeBlock?(element: CodeBlock): void;} // Wrapper that calls methods if they existclass VisitorAdapter implements DocumentVisitor { constructor(private partial: PartialDocumentVisitor) {} visitParagraph(element: Paragraph): void { this.partial.visitParagraph?.(element); } visitHeading(element: Heading): void { this.partial.visitHeading?.(element); } // ... etc} // Usage: Only implement needed methodsconst collector = new VisitorAdapter({ visitHeading: (h) => console.log(`Found: ${h.text}`)});accept part of the element interface—all elements must implement it.accept—it cannot be inherited.getResult() method.reset() for reuse without reinstantiation.Let's consolidate the key concepts of the Visitor pattern solution:
You now understand how the Visitor pattern achieves double dispatch, enabling new operations to be added without modifying element classes. You've seen the complete pattern structure, multiple implementation approaches for return values and traversal, and practical guidelines. Next, we'll explore when to use the Visitor pattern—and importantly, when not to.