Interpreter
Represent the grammar of a small language as classes, so its sentences can be evaluated.
Use it when
You need to evaluate simple rules or expressions, such as filters, formulas or discount conditions.
/**
* Interpreter: evaluate simple discount rules like "total > 100 and member".
*
* Real-world scenario: a shop lets the marketing team define discount rules
* without changing code. Each part of a rule is a small class: constants and
* variables are the leaves, and "greater than" and "and" combine them. A rule is
* a tree of these objects, and evaluating it means asking the root to interpret
* itself against a customer's cart.
*
* The rule trees are built by hand here; a real app would parse rule text into
* the same objects.
*/
type Value = number | boolean;
type Context = Record<string, Value>;
// Expression: every part of a rule can interpret itself against a context.
interface Expression {
interpret(context: Context): Value;
toString(): string;
}
// Terminal expressions: the leaves of the tree.
// (Named Constant because Number is a built-in JavaScript type.)
class Constant implements Expression {
private readonly value: number;
constructor(value: number) {
this.value = value;
}
interpret(): Value {
return this.value;
}
toString(): string {
return String(this.value);
}
}
class Variable implements Expression {
private readonly name: string;
constructor(name: string) {
this.name = name;
}
interpret(context: Context): Value {
return context[this.name];
}
toString(): string {
return this.name;
}
}
// Non-terminal expressions: combine other expressions.
class GreaterThan implements Expression {
private readonly left: Expression;
private readonly right: Expression;
constructor(left: Expression, right: Expression) {
this.left = left;
this.right = right;
}
interpret(context: Context): Value {
return Number(this.left.interpret(context)) > Number(this.right.interpret(context));
}
toString(): string {
return `${this.left} > ${this.right}`;
}
}
class And implements Expression {
private readonly left: Expression;
private readonly right: Expression;
constructor(left: Expression, right: Expression) {
this.left = left;
this.right = right;
}
interpret(context: Context): Value {
return Boolean(this.left.interpret(context)) && Boolean(this.right.interpret(context));
}
toString(): string {
return `${this.left} and ${this.right}`;
}
}
function clientCode(): void {
// Two rules built from the same small set of classes.
const memberDeal = new And(
new GreaterThan(new Variable("total"), new Constant(100)),
new Variable("member"),
);
const bulkDeal = new GreaterThan(new Variable("items"), new Constant(5));
const carts: Record<string, Context> = {
Asha: { total: 150, items: 2, member: true },
Ravi: { total: 150, items: 8, member: false },
Meena: { total: 80, items: 3, member: true },
};
for (const rule of [memberDeal, bulkDeal]) {
console.log(`Rule: ${rule}`);
for (const [customer, cart] of Object.entries(carts)) {
const result = rule.interpret(cart) ? "gets discount" : "no discount";
console.log(` ${customer}: ${result}`);
}
}
}
clientCode();
// Output:
// Rule: total > 100 and member
// Asha: gets discount
// Ravi: no discount
// Meena: no discount
// Rule: items > 5
// Asha: no discount
// Ravi: gets discount
// Meena: no discountHow it works
| Role | Classes | What it does |
|---|---|---|
| Abstract expression | Expression | Interface: every part of a rule can interpret() itself against a context |
| Terminal expressions | Constant, Variable | The leaves: a fixed number, or a value looked up in the cart |
| Non-terminal expressions | GreaterThan, And | Combine other expressions; nesting them builds a whole rule |
| Context | Context (Record) | The customer's data the rule is evaluated against |
| Client | clientCode() | Builds rule trees and evaluates them for each customer |