01 / The idea
Where should the next operation live?
Giving each node an export method is a reasonable start. A text node returns its text, an image contributes its description, and a group combines its children. The operation is easy to find beside the data it uses.
Then the inventory arrives, and the description review after it. If each one adds a method to every node, each workflow ends up spread across the document model. You want to change how export works without opening the files that define what a document is.
Visitor puts an operation in a separate object with a method for each supported node type. A node accepts that object and routes the call to its corresponding method. The operation can then keep its rules and accumulated result together.
So the visitor knows the document’s vocabulary, and each node knows which visitor method is its own. That makes a new operation cheap: you write one more visitor. A new kind of node is the expensive change, because every visitor needs a method for it.
Describe the content.
Text, Image, and Group hold the article’s structure and route a visit to the matching method.
Own one operation.
Export, inventory, or review supplies its own rules, traversal decisions, and accumulated output.
Choose and run the work.
Create a fresh operation, pass it to the root, and consume the result after the run finishes.
02 / See the shape
The node chooses a handler. The handler owns the policy.
Follow an Image node: accept receives the operation, then calls its image method with the node. Text export reads the description; inventory reads the source; review checks the relationship between description and decorative intent.
Group.accept only dispatches to the group method. Each visitor decides whether to walk the children. Text export stops at a draft group. Inventory includes unpublished assets, and review includes unpublished images. Children are visited in their stored order.
The basic view shows this dispatch contract. The practical view adds all three operations and their traces. The call site runs them over the same article; complete files include everything needed to reproduce the output.
Three node variants and three required visitor methods. Accept selects the matching method; it does not walk the children. The implementations use native dispatch mechanisms.
export interface Visitor {
visitText(node: TextNode): void;
visitImage(node: ImageNode): void;
visitGroup(node: GroupNode): void;
}
export interface DocumentNode {
readonly id: string;
accept(visitor: Visitor): void;
}
export class TextNode implements DocumentNode {
readonly id: string;
readonly text: string;
constructor(id: string, text: string) {
this.id = id;
this.text = text;
}
accept(visitor: Visitor) {
visitor.visitText(this);
}
}
export class ImageNode implements DocumentNode {
readonly id: string;
readonly src: string;
readonly alt: string;
readonly decorative: boolean;
constructor(id: string, src: string, alt: string, decorative: boolean) {
this.id = id;
this.src = src;
this.alt = alt;
this.decorative = decorative;
}
accept(visitor: Visitor) {
visitor.visitImage(this);
}
}
export class GroupNode implements DocumentNode {
readonly id: string;
readonly draft: boolean;
readonly children: readonly DocumentNode[];
constructor(id: string, draft: boolean, children: readonly DocumentNode[]) {
this.id = id;
this.draft = draft;
this.children = [...children];
}
// Dispatch only. This method does not walk the children.
accept(visitor: Visitor) {
visitor.visitGroup(this);
}
} type Visitor interface {
VisitText(*TextNode)
VisitImage(*ImageNode)
VisitGroup(*GroupNode)
}
type DocumentNode interface{ Accept(Visitor) }
type TextNode struct{ ID, Text string }
func (n *TextNode) Accept(v Visitor) { v.VisitText(n) }
type ImageNode struct {
ID, Src, Alt string
Decorative bool
}
func (n *ImageNode) Accept(v Visitor) { v.VisitImage(n) }
type GroupNode struct {
ID string
Draft bool
Children []DocumentNode
}
// Dispatch only. The visitor decides whether to walk children.
func (n *GroupNode) Accept(v Visitor) { v.VisitGroup(n) } Reading the TypeScriptAn interface for operations, a narrow accept method for nodes
Each class’s accept method names the corresponding visitor method. That method receives the concrete node type, so visitImage can read src and alt without inspecting a tag. The first call is chosen by the node’s runtime type and the second by the visitor’s; this is the classic double-dispatch arrangement.
A switch over a discriminated union would also work; section 06 compares it. The example uses accept so that an operation is one object implementing Visitor. When the interface gains a method, TypeScript reports each visitor class that lacks it.
Readonly fields are a compile-time rule, not a deep runtime freeze. Group copies the child array’s membership, but it retains the child objects. The supplied visitors only read the tree; they keep mutable output arrays in their own instances.
Reading the GoInterfaces provide dispatch without a base class
DocumentNode requires Accept. TextNode, ImageNode, and GroupNode each implement it with a pointer receiver. Their method bodies select VisitText, VisitImage, or VisitGroup on the Visitor interface.
The visitor’s pointer retains its output while child calls run. Embedded Report fields keep results together; the asset visitor also owns a map for membership checks. Output order comes from the appended slice, not map iteration. Go does not make the node pointers read-only: these visitors follow that contract by convention.
03 / Follow the walk
The same root can produce a different walk.
Run text export with the notes kept as draft. Then switch to inventory. Predict whether draft-image is visited, even though its source has already appeared on hero. A callback can happen without adding an output entry.
Try an empty hero description, or mark it decorative while keeping a description. Compare text export with image review: omitting text and reporting a description issue are different policies.
One document, three operations
Keep the document in view. Choose an operation and predict whether draft-image will receive a callback, then inspect the recorded visit sequence.
Text and nonempty informative image descriptions become lines. Draft groups stop traversal; decorative images contribute no line.
Document tree
9 nodesThe structure stays the same while the operation changes.
pagegroup—Published group
headingtext—Visitor in practice
heroimage—/cover.png · informative
bodygroup—Published group
introtext—One document, several operations.
dividerimage—/divider.svg · decorative
draftgroup—Draft group
draft-notetext—Unpublished notes
draft-imageimage—/cover.png · informative
Export readable text
Ready to runRun the operation to see its output and follow the callbacks. Changing a document input clears the previous result.
04 / Try a decision
A new operation and a new node are different changes.
A word-count visitor can implement the existing three methods. The node definitions need no word-count method. Setup still needs to expose or call that new operation.
Video changes the vocabulary itself. Every operation now needs a policy for something it did not previously understand. This is the other half of the design, and a useful test of whether it fits the changes you expect.
Reasoning and a stopping pointAlso available without JavaScript
Adding an operation usually means adding one visitor implementation. Adding a node type changes the vocabulary understood by every operation. In this design, add the handler to the contract, route Video to it, and make each operation’s policy explicit—even if that policy is deliberately “ignore this variant.”
TypeScript and Go check required methods after the visitor interface changes: TypeScript reports each visitor class that lacks the new method, and Go reports each place a visitor is passed as a Visitor. They cannot infer that an unrelated new class or struct must receive a new handler. A closed set of variants can require a new branch when a variant is added. None of those checks proves that a caption policy is correct.
If your node types change every week while the operations are stable, methods on nodes or a simpler set of functions may fit better. A visitor is a decision about which kind of change you want to concentrate.
Name one operation you could add without changing the nodes. Then name every decision that adding Video would require.
This reflection is not saved or automatically assessed.05 / Give it a real job
Run a fresh operation over a stable document.
An export button, build task, or review command can pass the current document to runOperation. It creates a fresh visitor and returns that run’s output and callback trace. Reusing a TextExport instance would append another run’s lines to the previous ones; the helper deliberately avoids that lifetime.
The supplied operations read the tree without changing it, synchronously, over trusted finite input. An image source is only a string: inventory neither normalizes URLs nor checks files.
Review has two authored rules: flag an empty description on an informative image, and flag a nonempty one on an image marked decorative. Whitespace is preserved. Passing them shows that those two conditions hold, not that the descriptions are good or the document accessible.
| Production change | Decision to make |
|---|---|
| Add another report over the same nodes | Add an operation implementation and wire its caller. Give each existing node kind an explicit policy. |
| Add a Video node | Extend the node vocabulary, dispatch, and visitor contract. Review every operation’s behavior for Video. |
| Process documents from outside the application | Validate structure, permitted values, size, and nesting before traversal. |
| Write files or fetch data during a visit | Define error propagation, cancellation, partial results, and side effects. |
Who owns recursion—and what happens to shared nodes?Dispatch and traversal are separate contracts
In this sample, group visitor methods own recursion. Adding another child loop inside Group.accept would visit descendants again. A separate walker can be useful when operations share one traversal policy, but its contract must explain how a handler continues, skips, or replaces that walk.
The recursive calls use the runtime call stack, so arbitrarily deep input can exhaust it. TypeScript and Go can also be given shared node references or cycles. These operations count each occurrence along a path and do not detect cycles. An owned tree representation may not preserve shared node identity in the same way.
Asset deduplication is a separate policy about source strings. Two image nodes can both receive callbacks while contributing one asset.
When a visitor fails partwayExceptions, partial output, and side effects
An unexpected exception or panic stops the run without a successful report or any rollback. The supplied operations only read, so the input survives; a new visitor with side effects would leave those effects in place.
If a report can fail for expected reasons, choose a result or error contract and decide whether partial output is meaningful. For a transformation, returning a new document is a different responsibility from collecting this report.
06 / Make the call
A visitor is one way to keep operations separate.
A TypeScript discriminated union can also support ordinary functions that switch on each variant. Each function remains a separate operation. For a small vocabulary and a few functions, that can be the clearest arrangement.
The visitor contract becomes useful when operations need a shared extension interface, reusable traversal hooks, or state that lasts across a walk. A match-based implementation can select the method, while the visitor contract describes what an operation must provide.
Required visitor methods expose missing implementations after the contract is updated. Default handlers can make partial visitors convenient while weakening that coverage signal.
Keep behavior on nodes when it belongs to their responsibilities and adding node kinds is the common change. Keep direct functions when they express the job clearly. Reach for Visitor when keeping operations together earns the extra dispatch contract.
07 / Already in your toolbox
Read the callback contract before writing the handler.
Go’s ast.Visitor works with ast.Walk. Visit returns the visitor to use for children; returning nil skips them. Walk also sends a nil-node callback after visiting children. That library owns the walk, unlike our group methods.
Build UIs?Every lint rule you run visits your source one node type at a time, and one day your team will need a rule of its own.
Where it already is in your components
The lint rules you run on every save are written this way. An ESLint rule’s create() “Returns an object with methods that ESLint calls to "visit"
nodes while traversing the abstract syntax tree”. Each key is a node type or a selector,
and a key ending in :exit runs on the way back up, after the node’s children. Like
Go’s ast.Walk, ESLint owns the walk, and a rule lists only the node types it cares about, where
our visitors must handle all three.
Babel traverse works the same way for code transformations: the caller provides handlers for particular node types and the library supplies the traversal, though its paths and mutation facilities go well beyond this document visitor.
When you have to own it
Now your team wants a rule the plugins do not ship, such as flagging an image component
used without a description. You write the create() object: one method for the node
type that matters, one decision about what to report, and nothing about how the tree is walked.
That is this lesson’s split, with the linter as the owner of traversal and your rule as one
operation over a vocabulary you did not define.
08 / Take the idea with you
Which kind of change should have one home?
Explain the design without saying “Visitor”: these nodes describe the document, this operation knows what each kind means, and this part owns the walk. Then name one new operation that stays together—and one new node type that would make you inspect every operation.
Choose Visitor when a known vocabulary keeps gaining independent operations, and you are willing to revisit those operations when the vocabulary changes.
Connections to follow nextRelated lessons
Composite supplies a common contract for individual elements and groups. Visitor supplies operations over those shapes. They can appear together, but a visitor can also work over a flat collection of variants. Interpreter is the contrast: each expression interprets itself, so evaluation lives in the node types. Here the node types only route a call, and each operation lives outside them.
Discriminated unions make the alternatives explicit and support matching. Strategy supplies an interchangeable policy; Visitor additionally gives an operation a method for each supported shape. Mediator owns collaboration rules between participants; it does not inherently define a walk over node variants.