+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 297 of 355

๐Ÿ“˜ Interpreter Pattern: Domain Language

Master interpreter pattern: domain language in TypeScript with practical examples, best practices, and real-world applications ๐Ÿš€

๐Ÿš€Intermediate
25 min read

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:

  1. Domain-Specific Languages ๐Ÿ”’: Create languages tailored to your problem
  2. Flexibility ๐Ÿ’ป: Users can write expressions without changing code
  3. Extensibility ๐Ÿ“–: Easy to add new operations and expressions
  4. 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

  1. ๐ŸŽฏ Keep Grammar Simple: Start with basic expressions and grow
  2. ๐Ÿ“ Document Your Language: Users need to know the syntax
  3. ๐Ÿ›ก๏ธ Validate Everything: Check inputs before interpreting
  4. ๐ŸŽจ Use Abstract Syntax Trees: Separate parsing from execution
  5. โœจ 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:

  1. ๐Ÿ’ป Practice with the exercises above
  2. ๐Ÿ—๏ธ Build a mini query language for your app
  3. ๐Ÿ“š Move on to our next tutorial: Iterator Pattern
  4. ๐ŸŒŸ 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! ๐ŸŽ‰๐Ÿš€โœจ