Interpreter Pattern
Defines a representation for a language's grammar along with an interpreter that uses the representation to interpret sentences in the language. Interpreter is used to build domain-specific languages (DSLs) and expression evaluators.
✦ Problem
When a particular type of problem occurs frequently, it can be worthwhile to express instances of the problem as sentences in a simple language. If you hardcode the logic for interpreting each possible expression, the code becomes rigid and impossible to extend without modification. Each new expression type requires changes throughout the system.
✦ Solution
Represent the grammar as a class hierarchy where each rule is a class. Terminal expressions represent literals and variables. Non-terminal expressions combine other expressions using grammar rules. Each expression class implements an interpret method that evaluates the expression in a given context. New rules are added by creating new expression classes.
Participants
| Role | Responsibility |
|---|---|
| AbstractExpression | Declares an interpret method common to all nodes in the AST |
| TerminalExpression | Implements interpret for terminal symbols in the grammar |
| NonterminalExpression | Maintains references to child expressions and implements interpret for grammar rules |
| Context | Contains global information needed during interpretation (variables, environment) |
TypeScript Example
interface Expression {
interpret(context: Record<string, boolean>): boolean;
toString(): string;
}
class Variable implements Expression {
constructor(private name: string) {}
interpret(ctx: Record<string, boolean>) { return ctx[this.name] ?? false; }
toString() { return this.name; }
}
class And implements Expression {
constructor(private left: Expression, private right: Expression) {}
interpret(ctx: Record<string, boolean>) {
return this.left.interpret(ctx) && this.right.interpret(ctx);
}
toString() { return `(${this.left} AND ${this.right})`; }
}
class Or implements Expression {
constructor(private left: Expression, private right: Expression) {}
interpret(ctx: Record<string, boolean>) {
return this.left.interpret(ctx) || this.right.interpret(ctx);
}
toString() { return `(${this.left} OR ${this.right})`; }
}
class Not implements Expression {
constructor(private expr: Expression) {}
interpret(ctx: Record<string, boolean>) {
return !this.expr.interpret(ctx);
}
toString() { return `NOT ${this.expr}`; }
}
// Rule: (isAdmin OR (isLoggedIn AND NOT isBanned))
const rule = new Or(
new Variable("isAdmin"),
new And(
new Variable("isLoggedIn"),
new Not(new Variable("isBanned")),
),
);
console.log(rule.toString());
console.log(rule.interpret({ isAdmin: false, isLoggedIn: true, isBanned: false })); // true
console.log(rule.interpret({ isAdmin: false, isLoggedIn: true, isBanned: true })); // false
console.log(rule.interpret({ isAdmin: true, isLoggedIn: false, isBanned: true })); // trueReal-World Example
Regular expression engines interpret pattern syntax. SQL parsers interpret query strings. CSS selectors are interpreted to match DOM elements. Template engines like Handlebars and EJS interpret template syntax. Rule engines in business applications interpret policy rules. Math expression parsers in calculators and spreadsheets use the Interpreter pattern to evaluate formulas.
✓ When to Use
- •The grammar is simple and efficiency is not a critical concern
- •You need to interpret expressions in a domain-specific language
- •You want to define rules declaratively and evaluate them at runtime
- •The grammar doesn't change frequently but the expressions evaluated change often
✗ When Not to Use
- •The grammar is complex (use parser generators like ANTLR instead)
- •Efficiency is critical—Interpreter creates deep object trees that are slow to traverse
- •The language evolves frequently, requiring constant class hierarchy changes
Understanding the Interpreter Pattern
The Interpreter pattern is one of the most important behavioral design patterns in object-oriented software design. Originally cataloged by the Gang of Four (Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides) in their seminal 1994 book "Design Patterns: Elements of Reusable Object-Oriented Software," this pattern continues to be a cornerstone of modern software architecture.
Defines a representation for a language's grammar along with an interpreter that uses the representation to interpret sentences in the language. Interpreter is used to build domain-specific languages (DSLs) and expression evaluators. The pattern involves the following key participants: AbstractExpression, TerminalExpression, NonterminalExpression, Context. Each participant has a well-defined role, and the interactions between them create a flexible, maintainable structure that can evolve with changing requirements.
In TypeScript and modern JavaScript development, the Interpreter pattern is particularly valuable because the language's type system and interface support allow you to express the pattern with compile-time safety. TypeScript generics can make Interpreter implementations even more flexible and reusable while maintaining strong type checking throughout the pattern's structure.
Knowing when to apply the Interpreter pattern is as important as knowing how. Common use cases include: The grammar is simple and efficiency is not a critical concern; You need to interpret expressions in a domain-specific language; You want to define rules declaratively and evaluate them at runtime. However, overusing design patterns can lead to unnecessarily complex code. Always evaluate whether the pattern genuinely solves your problem or if a simpler approach would suffice.
The Interpreter pattern is related to several other design patterns: Composite, Visitor, Iterator, Flyweight. Understanding these relationships helps you choose the right pattern for your specific situation and combine patterns effectively when building complex systems.
Related Patterns
Composes objects into tree structures to represent part-whole hierarchies. Composite lets clients treat individual objec…
Represents an operation to be performed on elements of an object structure. Visitor lets you define new operations witho…
Provides a way to access the elements of an aggregate object sequentially without exposing its underlying representation…
Uses sharing to support large numbers of fine-grained objects efficiently. Flyweight reduces memory usage by sharing as …
More Behavioral Patterns
Explore All Design Patterns
Browse our complete reference of 23 design patterns with TypeScript examples, UML participants, and practical guidance.