# Visitor and Double Dispatch

> Why overloading alone cannot pick a method by an argument's runtime type, how Visitor uses double dispatch to fix it, and when pattern matching replaces it.

- kind: guide
- category: design-patterns
- aliases: Double dispatch, Single dispatch
- canonical: https://aipatterns.dev/guides/visitor-and-double-dispatch
- inspired-by: https://refactoring.guru/design-patterns/visitor-double-dispatch

## The problem

You have a tree of node types (say, invoice line items) and you need many operations over them: compute tax, render HTML, export to an accounting system. You don't want each node class to grow a method for every operation. You want each operation in one place.

The natural first attempt is a class per operation with one overload per node type. In most mainstream languages, that attempt fails quietly.

## Single dispatch

Languages like Java, C# and C++ pick a method in two stages:

- **Which object's method** is chosen at runtime, from the receiver's actual type. This is ordinary polymorphism, called *single dispatch*.
- **Which overload** is chosen at compile time, from the argument's *declared* type.

```java
interface Item {}
class Product implements Item {}
class Shipping implements Item {}

class TaxCalculator {
    long tax(Product p)  { return 20; }
    long tax(Shipping s) { return 5; }
    long tax(Item i)     { return 0; }
}

Item item = new Product();
new TaxCalculator().tax(item); // returns 0, not 20
```

The compiler sees `Item`, so it binds to `tax(Item)`. The runtime type `Product` is never consulted for overload selection. TypeScript and Python don't have this trap because they have no runtime overloading at all; there you would write a type check by hand.

## Double dispatch

The fix is to make two runtime dispatches in a row. First, call a method on the *element*, so its real type is known inside it. Then the element calls back to the operation, passing `this`, whose static type is now the concrete class.

```ts
interface ItemVisitor<R> {
  visitProduct(p: Product): R;
  visitShipping(s: Shipping): R;
}

interface Item {
  accept<R>(v: ItemVisitor<R>): R;
}

class Product implements Item {
  constructor(readonly price: number, readonly taxRate: number) {}
  accept<R>(v: ItemVisitor<R>): R { return v.visitProduct(this); }
}

class Shipping implements Item {
  constructor(readonly fee: number) {}
  accept<R>(v: ItemVisitor<R>): R { return v.visitShipping(this); }
}

class TaxVisitor implements ItemVisitor<number> {
  visitProduct(p: Product) { return p.price * p.taxRate; }
  visitShipping(s: Shipping) { return s.fee * 0.05; }
}

const items: Item[] = [new Product(100, 0.2), new Shipping(10)];
const tax = items.reduce((sum, i) => sum + i.accept(new TaxVisitor()), 0); // 20.5
```

Dispatch one picks `Product.accept`. Dispatch two picks `visitProduct` on whichever visitor was passed. The operation now depends on two runtime types: the element's and the visitor's.

```mermaid
sequenceDiagram
  participant Client
  participant Product
  participant TaxVisitor
  Client->>Product: accept(taxVisitor)
  Product->>TaxVisitor: visitProduct(this)
  TaxVisitor-->>Product: tax amount
  Product-->>Client: tax amount
```

This is the core of the [Visitor](https://aipatterns.dev/design-patterns/visitor.md) pattern.

## Trade-off: which axis is cheap to extend

| Change | Visitor | Methods on each node |
|---|---|---|
| Add an operation | One new visitor class | Edit every node class |
| Add a node type | Edit every visitor | One new class |

**Use Visitor when the set of node types is stable and operations keep growing.** Compilers, AST tools and document exporters fit. If node types change often, put the method on each node instead, using [Replace Conditional with Polymorphism](https://aipatterns.dev/refactoring/techniques/replace-conditional-with-polymorphism.md).

## When pattern matching replaces Visitor

Languages with sealed types or discriminated unions plus exhaustive matching give you the same guarantees with less code. TypeScript can do this with a discriminated union and a `never` check:

```ts
type Item =
  | { kind: "product"; price: number; taxRate: number }
  | { kind: "shipping"; fee: number };

function tax(item: Item): number {
  switch (item.kind) {
    case "product":  return item.price * item.taxRate;
    case "shipping": return item.fee * 0.05;
    default: {
      const unreachable: never = item; // compile error if a kind is missing
      return unreachable;
    }
  }
}
```

Java 21 sealed interfaces with `switch` patterns, Kotlin `when` over sealed classes, Rust `match` and Python `match` with type checkers all offer equivalents. The compiler flags every unhandled case when a new kind is added, which is the main safety Visitor provides.

Prefer pattern matching when:

- The data types are plain records with no behavior of their own.
- The language checks exhaustiveness.
- You don't need visitors as swappable objects at runtime.

Keep a classic Visitor when the element types are classes in a hierarchy you can't turn into a union, or when visitors carry state and are passed around as values.

## Related

- [Composite](https://aipatterns.dev/design-patterns/composite.md): the tree that a Visitor usually walks.
- [Iterator](https://aipatterns.dev/design-patterns/iterator.md): separates traversal order from the visitor's per-node work.
- [Switch Statements](https://aipatterns.dev/refactoring/smells/switch-statements.md): a type switch that is repeated in many places is the smell Visitor or polymorphism addresses. One exhaustive switch in one place is not a smell.
- [Classification of patterns](https://aipatterns.dev/guides/classification-of-patterns.md): where Visitor sits among behavioral patterns.
