ast-visitor-pattern
$
npx mdskill add prisma/prisma-next/ast-visitor-patternReplace switch statements with frozen classes and visitor patterns
- Solves silent errors when adding variants to multi-site discriminated unions
- Depends on TypeScript abstract classes, interfaces, and Object.freeze()
- Decides behavior by enforcing compile-time checks for missing method calls
- Delivers type safety through frozen subclasses that reject invalid operations
SKILL.md
.github/skills/ast-visitor-patternView on GitHub ↗
---
name: ast-visitor-pattern
description: >-
Use the frozen-class/visitor pattern for discriminated unions that have
multiple dispatch sites. Use when creating a new set of variants
(commands, IR nodes, factory calls) that will be switched over in 2+
places, or when refactoring an existing union type that has grown
multiple switch sites.
---
# AST Class/Visitor Pattern
When a discriminated union has **3+ variants** and **2+ dispatch sites** (renderers, serializers, classifiers, etc.), replace plain union + switch with frozen subclasses and a visitor interface. This makes adding a new variant a compiler error at every consumer, instead of a silent omission.
## Structure
Four pieces, always in the same file:
```typescript
// 1. Abstract base (not exported — consumers use the union type)
abstract class FooNode {
abstract readonly kind: string;
abstract accept<R>(visitor: FooVisitor<R>): R;
protected freeze(): void { Object.freeze(this); }
}
// 2. Visitor interface
export interface FooVisitor<R> {
bar(node: BarNode): R;
baz(node: BazNode): R;
}
// 3. Concrete subclasses — readonly fields, freeze() in constructor
export class BarNode extends FooNode {
readonly kind = 'bar' as const;
readonly value: string;
constructor(value: string) {
super();
this.value = value;
this.freeze();
}
accept<R>(visitor: FooVisitor<R>): R { return visitor.bar(this); }
}
export class BazNode extends FooNode {
readonly kind = 'baz' as const;
readonly count: number;
constructor(count: number) {
super();
this.count = count;
this.freeze();
}
accept<R>(visitor: FooVisitor<R>): R { return visitor.baz(this); }
}
// 4. Union type
export type Foo = BarNode | BazNode;
```
## Consuming
Define a visitor object (or class) per concern:
```typescript
const renderVisitor: FooVisitor<string> = {
bar(node) { return node.value; },
baz(node) { return String(node.count); },
};
function render(node: Foo): string {
return node.accept(renderVisitor);
}
```
## In tests
Always construct subclass instances, never plain objects:
```typescript
// ✅
const call = new BarNode('x');
// ❌
const call: Foo = { kind: 'bar', value: 'x' };
```
## When NOT to use
- Single dispatch site → plain union + switch is simpler
- Fewer than 3 variants with no expected growth → not worth the boilerplate
## Codebase examples
- `MongoAstNode` / `MongoDdlCommandVisitor` — `packages/2-mongo-family/4-query/query-ast/src/ddl-commands.ts`
- `OpFactoryCall` / `OpFactoryCallVisitor` — `packages/3-mongo-target/1-mongo-target/src/core/op-factory-call.ts`
More from prisma/prisma-next
- adr-review>-
- bumping-biomeBumps `biome` package versions (e.g. `@biomejs/biome`) using `pnpm`, aligns `biome.jsonc` files with the new version/s across the repository and runs biome-related checks. Use when required to update `biome` to a newer version - explicitly or implicitly (e.g. after running `pnpm up`, `pnpm update`, `pnpm upgrade` without specific package names).
- contrib-prOpen a high-quality external contributor PR against prisma-next. Use when the user is an outside contributor (not a Prisma maintainer) and wants to submit a change as a pull request from a fork. Encodes the contribution flow from CONTRIBUTING.md so the resulting PR passes review on the first round.
- drive-agent-personasLibrary of agent personas — named bias-frames that other skills load to shift execution-time defaults. Skills name a persona by ID (e.g. "Adopt the architect persona"), and this skill resolves that ID to the persona doc that frames the executor for the rest of the task. Use when authoring a new skill that needs a particular reviewer/implementer/orchestrator stance, or when an existing skill instructs you to adopt a named persona.
- drive-create-plan>
- drive-create-project>
- drive-create-spec>
- drive-discussionDrops the agent into a structured Q&A mode that iterates with the user toward a complete understanding of a topic, then documents the outcome (project spec, plan, decision record, or whatever shape fits). The agent adopts one or more personas from the `drive-agent-personas` library — named explicitly by the user, or inferred from conversation context and announced. Typical use is design work at the start of a task, or mid-implementation when a load-bearing assumption has been falsified. Use ONLY when the user explicitly invokes this skill (e.g. "discussion mode", "pressure-test this", "let's design this", "design mode", "tech design mode", "product mode", "pm mode", "challenge my idea"). Never auto-invoke.
- drive-orchestrate-plan>
- drive-pr-descriptionGenerates PR descriptions by analyzing git diffs between the current branch and the default branch. Use when the user requests a PR description, pull request summary, or commit message for a squash merge.