Prerequisites
- Basic understanding of JavaScript ๐
- TypeScript installation โก
- VS Code or preferred IDE ๐ป
What you'll learn
- Understand the concept fundamentals ๐ฏ
- Apply the concept in real projects ๐๏ธ
- Debug common issues ๐
- Write type-safe code โจ
๐ฏ Introduction
Welcome to this exciting tutorial on the Interpreter Pattern! ๐ In this guide, weโll explore how to create your own mini programming languages and expression evaluators using TypeScript.
Youโll discover how the Interpreter Pattern can transform complex string expressions into executable code. Whether youโre building query languages ๐, formula calculators ๐งฎ, or configuration DSLs ๐, understanding this pattern is essential for creating flexible, extensible systems.
By the end of this tutorial, youโll feel confident implementing your own domain-specific languages! Letโs dive in! ๐โโ๏ธ
๐ Understanding Interpreter Pattern
๐ค What is the Interpreter Pattern?
The Interpreter Pattern is like having a translator for your own custom language ๐. Think of it as creating a mini programming language that understands specific commands for your application.
In TypeScript terms, it defines a grammar for a language and provides an interpreter to process sentences in that language. This means you can:
- โจ Create custom query languages
- ๐ Build expression evaluators
- ๐ก๏ธ Parse and execute domain-specific commands
๐ก Why Use the Interpreter Pattern?
Hereโs why developers love this pattern:
- Domain-Specific Languages ๐: Create languages tailored to your problem
- Flexibility ๐ป: Users can write expressions without changing code
- Extensibility ๐: Easy to add new operations and expressions
- Separation of Concerns ๐ง: Grammar separate from execution logic
Real-world example: Imagine building a spreadsheet formula engine ๐. With the Interpreter Pattern, you can parse formulas like โ=SUM(A1:A10)โ and execute them dynamically.
๐ง Basic Syntax and Usage
๐ Simple Example
Letโs start with a friendly math expression interpreter:
// ๐ Hello, Interpreter Pattern!
interface Expression {
interpret(): number;
}
// ๐ข Number expression
class NumberExpression implements Expression {
constructor(private value: number) {}
interpret(): number {
return this.value; // ๐ฏ Simply return the number
}
}
// โ Addition expression
class AddExpression implements Expression {
constructor(
private left: Expression,
private right: Expression
) {}
interpret(): number {
// โจ Add left and right expressions
return this.left.interpret() + this.right.interpret();
}
}
// ๐ฎ Let's use it!
const expression = new AddExpression(
new NumberExpression(5),
new NumberExpression(3)
);
console.log(expression.interpret()); // 8 ๐
๐ก Explanation: Each expression type knows how to interpret itself. Numbers return their value, and operations combine their sub-expressions!
๐ฏ Common Patterns
Here are patterns youโll use when building interpreters:
// ๐๏ธ Pattern 1: Abstract syntax tree node
abstract class ASTNode {
abstract evaluate(context: Map<string, any>): any;
}
// ๐จ Pattern 2: Context for variables
class Context {
private variables = new Map<string, any>();
set(name: string, value: any): void {
this.variables.set(name, value);
}
get(name: string): any {
return this.variables.get(name);
}
}
// ๐ Pattern 3: Parser result
interface ParseResult {
expression: Expression;
remainingInput: string;
}
๐ก Practical Examples
๐ Example 1: Shopping Discount Language
Letโs build a discount rule interpreter:
// ๐๏ธ Discount rule interpreter
interface DiscountRule {
evaluate(cart: ShoppingCart): number;
}
// ๐ Shopping cart context
interface ShoppingCart {
items: Array<{ name: string; price: number; quantity: number; category: string }>;
totalAmount: number;
customerType: "regular" | "premium" | "vip";
}
// ๐ฐ Percentage discount rule
class PercentageDiscount implements DiscountRule {
constructor(private percentage: number) {}
evaluate(cart: ShoppingCart): number {
console.log(`๐ฏ Applying ${this.percentage}% discount`);
return cart.totalAmount * (this.percentage / 100);
}
}
// ๐ฆ Category discount rule
class CategoryDiscount implements DiscountRule {
constructor(
private category: string,
private discount: number
) {}
evaluate(cart: ShoppingCart): number {
const categoryTotal = cart.items
.filter(item => item.category === this.category)
.reduce((sum, item) => sum + (item.price * item.quantity), 0);
console.log(`๐ท๏ธ ${this.discount}% off ${this.category} items`);
return categoryTotal * (this.discount / 100);
}
}
// ๐ Combine multiple rules
class CompositeDiscount implements DiscountRule {
constructor(private rules: DiscountRule[]) {}
evaluate(cart: ShoppingCart): number {
// ๐ฏ Sum all discounts
return this.rules.reduce((total, rule) =>
total + rule.evaluate(cart), 0
);
}
}
// ๐ฎ Let's use our discount language!
const cart: ShoppingCart = {
items: [
{ name: "TypeScript Book", price: 30, quantity: 2, category: "books" },
{ name: "Coffee Maker", price: 80, quantity: 1, category: "electronics" }
],
totalAmount: 140,
customerType: "premium"
};
const discountRules = new CompositeDiscount([
new PercentageDiscount(10), // 10% off everything
new CategoryDiscount("books", 20) // Extra 20% off books
]);
const totalDiscount = discountRules.evaluate(cart);
console.log(`๐ฐ Total discount: $${totalDiscount}`);
๐ฏ Try it yourself: Add a rule for VIP customers or minimum purchase amounts!
๐ฎ Example 2: Game Command Language
Letโs create a simple game command interpreter:
// ๐ฎ Game command interpreter
interface GameCommand {
execute(player: Player): void;
}
// ๐ญ Player context
class Player {
constructor(
public name: string,
public x: number = 0,
public y: number = 0,
public health: number = 100,
public inventory: string[] = []
) {}
status(): void {
console.log(`๐ ${this.name} at (${this.x}, ${this.y}) | โค๏ธ ${this.health} HP`);
console.log(`๐ Inventory: ${this.inventory.join(", ") || "empty"}`);
}
}
// ๐ถ Move command
class MoveCommand implements GameCommand {
constructor(
private direction: "north" | "south" | "east" | "west",
private distance: number = 1
) {}
execute(player: Player): void {
const moves = {
north: { x: 0, y: this.distance },
south: { x: 0, y: -this.distance },
east: { x: this.distance, y: 0 },
west: { x: -this.distance, y: 0 }
};
const move = moves[this.direction];
player.x += move.x;
player.y += move.y;
console.log(`๐ถ ${player.name} moved ${this.direction} ${this.distance} steps`);
}
}
// ๐ Pickup command
class PickupCommand implements GameCommand {
constructor(private item: string) {}
execute(player: Player): void {
player.inventory.push(this.item);
console.log(`โจ ${player.name} picked up ${this.item}!`);
}
}
// ๐ Heal command
class HealCommand implements GameCommand {
constructor(private amount: number) {}
execute(player: Player): void {
player.health = Math.min(100, player.health + this.amount);
console.log(`๐ ${player.name} healed ${this.amount} HP!`);
}
}
// ๐ Macro command (multiple commands)
class MacroCommand implements GameCommand {
constructor(private commands: GameCommand[]) {}
execute(player: Player): void {
console.log("๐ฌ Executing macro...");
this.commands.forEach(cmd => cmd.execute(player));
}
}
// ๐ Simple command parser
class CommandParser {
parse(input: string): GameCommand {
const parts = input.toLowerCase().split(" ");
const command = parts[0];
switch (command) {
case "move":
return new MoveCommand(
parts[1] as any,
parseInt(parts[2]) || 1
);
case "pickup":
return new PickupCommand(parts.slice(1).join(" "));
case "heal":
return new HealCommand(parseInt(parts[1]) || 20);
default:
throw new Error(`Unknown command: ${command} ๐`);
}
}
}
// ๐ฎ Game time!
const player = new Player("Hero");
const parser = new CommandParser();
const commands = [
"move north 2",
"pickup magic sword",
"move east",
"heal 30",
"pickup health potion"
];
commands.forEach(cmd => {
const command = parser.parse(cmd);
command.execute(player);
});
player.status();
๐ Advanced Concepts
๐งโโ๏ธ Building a Formula Language
When youโre ready to level up, try this advanced formula interpreter:
// ๐ฏ Advanced formula interpreter with variables
interface FormulaExpression {
evaluate(context: Map<string, number>): number;
}
// ๐ค Variable expression
class VariableExpression implements FormulaExpression {
constructor(private name: string) {}
evaluate(context: Map<string, number>): number {
const value = context.get(this.name);
if (value === undefined) {
throw new Error(`Variable ${this.name} not found! ๐ฑ`);
}
return value;
}
}
// ๐ฒ Function expression (SUM, AVG, MAX, etc.)
class FunctionExpression implements FormulaExpression {
constructor(
private func: string,
private args: FormulaExpression[]
) {}
evaluate(context: Map<string, number>): number {
const values = this.args.map(arg => arg.evaluate(context));
switch (this.func.toUpperCase()) {
case "SUM":
return values.reduce((a, b) => a + b, 0);
case "AVG":
return values.reduce((a, b) => a + b, 0) / values.length;
case "MAX":
return Math.max(...values);
case "MIN":
return Math.min(...values);
default:
throw new Error(`Unknown function: ${this.func} ๐คท`);
}
}
}
// โจ Formula evaluator
class FormulaEvaluator {
evaluate(formula: FormulaExpression, data: Record<string, number>): number {
const context = new Map(Object.entries(data));
return formula.evaluate(context);
}
}
๐๏ธ Creating a Query DSL
For the brave developers, hereโs a query language:
// ๐ Type-safe query builder
interface QueryExpression<T> {
matches(item: T): boolean;
}
// ๐ Comparison operators
class ComparisonExpression<T> implements QueryExpression<T> {
constructor(
private field: keyof T,
private operator: "=" | ">" | "<" | "!=" | "contains",
private value: any
) {}
matches(item: T): boolean {
const fieldValue = item[this.field];
switch (this.operator) {
case "=": return fieldValue === this.value;
case ">": return fieldValue > this.value;
case "<": return fieldValue < this.value;
case "!=": return fieldValue !== this.value;
case "contains":
return String(fieldValue).includes(this.value);
default: return false;
}
}
}
// ๐ Logical operators
class AndExpression<T> implements QueryExpression<T> {
constructor(private expressions: QueryExpression<T>[]) {}
matches(item: T): boolean {
return this.expressions.every(expr => expr.matches(item));
}
}
// ๐ฏ Query builder for type safety
class QueryBuilder<T> {
private expressions: QueryExpression<T>[] = [];
where(field: keyof T, operator: "=" | ">" | "<" | "!=" | "contains", value: any): this {
this.expressions.push(new ComparisonExpression(field, operator, value));
return this;
}
build(): QueryExpression<T> {
return new AndExpression(this.expressions);
}
}
โ ๏ธ Common Pitfalls and Solutions
๐ฑ Pitfall 1: Infinite Recursion
// โ Wrong way - no base case!
class BadExpression implements Expression {
interpret(): number {
return new BadExpression().interpret(); // ๐ฅ Stack overflow!
}
}
// โ
Correct way - proper termination
class GoodExpression implements Expression {
constructor(private value?: number) {}
interpret(): number {
if (this.value !== undefined) {
return this.value; // ๐ก๏ธ Base case
}
// Process sub-expressions...
return 0;
}
}
๐คฏ Pitfall 2: Missing Context Validation
// โ Dangerous - no validation!
class UnsafeVariable implements Expression {
constructor(private name: string) {}
interpret(context: any): number {
return context[this.name]; // ๐ฅ Could be undefined!
}
}
// โ
Safe - validate context
class SafeVariable implements Expression {
constructor(private name: string) {}
interpret(context: Map<string, number>): number {
if (!context.has(this.name)) {
console.log(`โ ๏ธ Variable '${this.name}' not found!`);
return 0; // Or throw an error
}
return context.get(this.name)!;
}
}
๐ ๏ธ Best Practices
- ๐ฏ Keep Grammar Simple: Start with basic expressions and grow
- ๐ Document Your Language: Users need to know the syntax
- ๐ก๏ธ Validate Everything: Check inputs before interpreting
- ๐จ Use Abstract Syntax Trees: Separate parsing from execution
- โจ Provide Clear Errors: Help users fix their expressions
๐งช Hands-On Exercise
๐ฏ Challenge: Build a CSS-like Style Language
Create a mini styling language interpreter:
๐ Requirements:
- โ Parse style rules like โcolor: red; size: 20pxโ
- ๐ท๏ธ Support different property types (color, size, position)
- ๐ค Apply styles to UI elements
- ๐ Support variables like โ$primary-colorโ
- ๐จ Each style needs validation!
๐ Bonus Points:
- Add nested selectors
- Implement style inheritance
- Create a style validator
๐ก Solution
๐ Click to see solution
// ๐ฏ Our style language interpreter!
interface StyleRule {
apply(element: UIElement): void;
}
// ๐จ UI Element to style
class UIElement {
public styles: Map<string, any> = new Map();
constructor(public name: string) {}
applyStyle(property: string, value: any): void {
this.styles.set(property, value);
console.log(`๐จ ${this.name}: ${property} = ${value}`);
}
showStyles(): void {
console.log(`๐ Styles for ${this.name}:`);
this.styles.forEach((value, prop) => {
console.log(` ${prop}: ${value}`);
});
}
}
// ๐จ Color style rule
class ColorRule implements StyleRule {
constructor(private color: string) {}
apply(element: UIElement): void {
element.applyStyle("color", this.color);
}
}
// ๐ Size style rule
class SizeRule implements StyleRule {
constructor(
private value: number,
private unit: "px" | "rem" | "%"
) {}
apply(element: UIElement): void {
element.applyStyle("size", `${this.value}${this.unit}`);
}
}
// ๐ค Variable storage
class StyleVariables {
private vars = new Map<string, string>();
set(name: string, value: string): void {
this.vars.set(name, value);
console.log(`๐พ Variable $${name} = ${value}`);
}
get(name: string): string {
const value = this.vars.get(name);
if (!value) {
throw new Error(`Variable $${name} not defined! ๐ฑ`);
}
return value;
}
}
// ๐ Style parser
class StyleParser {
constructor(private variables: StyleVariables) {}
parse(styleString: string): StyleRule[] {
const rules: StyleRule[] = [];
const declarations = styleString.split(";").filter(s => s.trim());
declarations.forEach(decl => {
const [property, value] = decl.split(":").map(s => s.trim());
// ๐ Check for variables
const resolvedValue = value.startsWith("$")
? this.variables.get(value.substring(1))
: value;
switch (property) {
case "color":
rules.push(new ColorRule(resolvedValue));
break;
case "size":
const match = resolvedValue.match(/(\d+)(px|rem|%)/);
if (match) {
rules.push(new SizeRule(
parseInt(match[1]),
match[2] as any
));
}
break;
}
});
return rules;
}
}
// ๐ฎ Test it out!
const variables = new StyleVariables();
variables.set("primary-color", "#007bff");
variables.set("base-size", "16px");
const parser = new StyleParser(variables);
const button = new UIElement("Button");
const styles = "color: $primary-color; size: 20px";
const rules = parser.parse(styles);
rules.forEach(rule => rule.apply(button));
button.showStyles();
๐ Key Takeaways
Youโve learned so much! Hereโs what you can now do:
- โ Create domain-specific languages with confidence ๐ช
- โ Build expression evaluators for complex rules ๐ก๏ธ
- โ Parse and interpret custom syntaxes ๐ฏ
- โ Apply the pattern to real-world problems ๐
- โ Design extensible language systems! ๐
Remember: The Interpreter Pattern lets you give your users a powerful way to express complex logic without changing code! ๐ค
๐ค Next Steps
Congratulations! ๐ Youโve mastered the Interpreter Pattern!
Hereโs what to do next:
- ๐ป Practice with the exercises above
- ๐๏ธ Build a mini query language for your app
- ๐ Move on to our next tutorial: Iterator Pattern
- ๐ Create your own domain-specific language!
Remember: Every powerful system started with simple expressions. Keep building, keep learning, and most importantly, have fun creating your own languages! ๐
Happy coding! ๐๐โจ