DevDocsDev Docs
Design PatternsBehavioral Patterns

Interpreter

Define a grammar and interpreter for a language

Interpreter Pattern

Intent

Interpreter is a behavioral design pattern that defines a grammatical representation for a language and provides an interpreter to deal with this grammar. It's used to evaluate sentences in a language.


Problem It Solves

When you need to interpret or evaluate a language:

  • Configuration files
  • Query languages (SQL-like)
  • Mathematical expressions
  • Domain-specific languages (DSL)
  • Regular expressions

Solution

Build an Abstract Syntax Tree (AST) where each node interprets itself:


Implementation

/**
 * Context holds variable values
 */
interface Context {
  variables: Map<string, number>;
  getVariable: (name: string) => number;
  setVariable: (name: string, value: number) => void;
}

const createContext = (): Context => {
  const variables = new Map<string, number>();
  
  return {
    variables,
    getVariable(name) {
      const value = variables.get(name);
      if (value === undefined) throw new Error(`Undefined variable: ${name}`);
      return value;
    },
    setVariable(name, value) {
      variables.set(name, value);
    },
  };
};

/**
 * Expression interface
 * @description All expressions must implement interpret
 */
interface Expression {
  interpret: (context: Context) => number;
  toString: () => string;
}

/**
 * Number literal
 */
const createNumber = (value: number): Expression => ({
  interpret: () => value,
  toString: () => String(value),
});

/**
 * Variable reference
 */
const createVariable = (name: string): Expression => ({
  interpret: (context) => context.getVariable(name),
  toString: () => name,
});

/**
 * Addition expression
 */
const createAdd = (left: Expression, right: Expression): Expression => ({
  interpret: (context) => left.interpret(context) + right.interpret(context),
  toString: () => `(${left.toString()} + ${right.toString()})`,
});

/**
 * Subtraction expression
 */
const createSubtract = (left: Expression, right: Expression): Expression => ({
  interpret: (context) => left.interpret(context) - right.interpret(context),
  toString: () => `(${left.toString()} - ${right.toString()})`,
});

/**
 * Multiplication expression
 */
const createMultiply = (left: Expression, right: Expression): Expression => ({
  interpret: (context) => left.interpret(context) * right.interpret(context),
  toString: () => `(${left.toString()} * ${right.toString()})`,
});

/**
 * Division expression
 */
const createDivide = (left: Expression, right: Expression): Expression => ({
  interpret: (context) => {
    const rightVal = right.interpret(context);
    if (rightVal === 0) throw new Error("Division by zero");
    return left.interpret(context) / rightVal;
  },
  toString: () => `(${left.toString()} / ${right.toString()})`,
});

/**
 * Power expression
 */
const createPower = (base: Expression, exponent: Expression): Expression => ({
  interpret: (context) => 
    Math.pow(base.interpret(context), exponent.interpret(context)),
  toString: () => `(${base.toString()} ^ ${exponent.toString()})`,
});

/**
 * Function expression
 */
const createFunction = (
  name: string,
  fn: (x: number) => number,
  arg: Expression
): Expression => ({
  interpret: (context) => fn(arg.interpret(context)),
  toString: () => `${name}(${arg.toString()})`,
});

// Math functions
const sqrt = (arg: Expression) => createFunction("sqrt", Math.sqrt, arg);
const sin = (arg: Expression) => createFunction("sin", Math.sin, arg);
const cos = (arg: Expression) => createFunction("cos", Math.cos, arg);
const abs = (arg: Expression) => createFunction("abs", Math.abs, arg);

// Build expression: (x + y) * 2 - sqrt(z)
const context = createContext();
context.setVariable("x", 5);
context.setVariable("y", 3);
context.setVariable("z", 16);

const expression = createSubtract(
  createMultiply(
    createAdd(createVariable("x"), createVariable("y")),
    createNumber(2)
  ),
  sqrt(createVariable("z"))
);

console.log("Expression:", expression.toString());
const result = expression.interpret(context);
console.log("Result:", result);
//                     ^?
// (5 + 3) * 2 - sqrt(16) = 16 - 4 = 12
/**
 * Context for boolean expressions
 */
interface BooleanContext {
  values: Map<string, boolean>;
  get: (name: string) => boolean;
  set: (name: string, value: boolean) => void;
}

const createBooleanContext = (): BooleanContext => {
  const values = new Map<string, boolean>();
  return {
    values,
    get(name) {
      const value = values.get(name);
      if (value === undefined) throw new Error(`Undefined: ${name}`);
      return value;
    },
    set(name, value) {
      values.set(name, value);
    },
  };
};

/**
 * Boolean expression interface
 */
interface BooleanExpression {
  evaluate: (context: BooleanContext) => boolean;
  toString: () => string;
}

/**
 * Literal true/false
 */
const createLiteral = (value: boolean): BooleanExpression => ({
  evaluate: () => value,
  toString: () => String(value),
});

/**
 * Variable reference
 */
const createBoolVar = (name: string): BooleanExpression => ({
  evaluate: (context) => context.get(name),
  toString: () => name,
});

/**
 * NOT expression
 */
const createNot = (expr: BooleanExpression): BooleanExpression => ({
  evaluate: (context) => !expr.evaluate(context),
  toString: () => `NOT(${expr.toString()})`,
});

/**
 * AND expression
 */
const createAnd = (left: BooleanExpression, right: BooleanExpression): BooleanExpression => ({
  evaluate: (context) => left.evaluate(context) && right.evaluate(context),
  toString: () => `(${left.toString()} AND ${right.toString()})`,
});

/**
 * OR expression
 */
const createOr = (left: BooleanExpression, right: BooleanExpression): BooleanExpression => ({
  evaluate: (context) => left.evaluate(context) || right.evaluate(context),
  toString: () => `(${left.toString()} OR ${right.toString()})`,
});

/**
 * XOR expression
 */
const createXor = (left: BooleanExpression, right: BooleanExpression): BooleanExpression => ({
  evaluate: (context) => left.evaluate(context) !== right.evaluate(context),
  toString: () => `(${left.toString()} XOR ${right.toString()})`,
});

/**
 * IMPLIES expression (A → B is equivalent to NOT A OR B)
 */
const createImplies = (left: BooleanExpression, right: BooleanExpression): BooleanExpression => ({
  evaluate: (context) => !left.evaluate(context) || right.evaluate(context),
  toString: () => `(${left.toString()} → ${right.toString()})`,
});

// Usage: Permission system
// canAccess = (isAdmin OR hasPermission) AND NOT isBlocked

const context = createBooleanContext();
context.set("isAdmin", false);
context.set("hasPermission", true);
context.set("isBlocked", false);

const canAccess = createAnd(
  createOr(
    createBoolVar("isAdmin"),
    createBoolVar("hasPermission")
  ),
  createNot(createBoolVar("isBlocked"))
);

console.log("Expression:", canAccess.toString());
console.log("Can access:", canAccess.evaluate(context));

// Change context
context.set("isBlocked", true);
console.log("After blocking:", canAccess.evaluate(context));

context.set("isAdmin", true);
context.set("isBlocked", false);
console.log("As admin:", canAccess.evaluate(context));
/**
 * Simple query language for filtering objects
 * Syntax: field operator value [AND|OR field operator value]
 */

interface QueryContext {
  data: Record<string, unknown>;
}

/**
 * Query expression interface
 */
interface QueryExpression {
  match: (context: QueryContext) => boolean;
  toString: () => string;
}

type Operator = "=" | "!=" | ">" | "<" | ">=" | "<=" | "contains" | "startsWith" | "endsWith";

/**
 * Comparison expression
 */
const createComparison = (
  field: string,
  operator: Operator,
  value: unknown
): QueryExpression => ({
  match(context) {
    const fieldValue = context.data[field];
    
    switch (operator) {
      case "=": return fieldValue === value;
      case "!=": return fieldValue !== value;
      case ">": return (fieldValue as number) > (value as number);
      case "<": return (fieldValue as number) < (value as number);
      case ">=": return (fieldValue as number) >= (value as number);
      case "<=": return (fieldValue as number) <= (value as number);
      case "contains": 
        return String(fieldValue).includes(String(value));
      case "startsWith":
        return String(fieldValue).startsWith(String(value));
      case "endsWith":
        return String(fieldValue).endsWith(String(value));
      default:
        return false;
    }
  },
  toString: () => `${field} ${operator} ${JSON.stringify(value)}`,
});

/**
 * AND expression
 */
const and = (left: QueryExpression, right: QueryExpression): QueryExpression => ({
  match: (context) => left.match(context) && right.match(context),
  toString: () => `(${left.toString()} AND ${right.toString()})`,
});

/**
 * OR expression
 */
const or = (left: QueryExpression, right: QueryExpression): QueryExpression => ({
  match: (context) => left.match(context) || right.match(context),
  toString: () => `(${left.toString()} OR ${right.toString()})`,
});

/**
 * NOT expression
 */
const not = (expr: QueryExpression): QueryExpression => ({
  match: (context) => !expr.match(context),
  toString: () => `NOT(${expr.toString()})`,
});

/**
 * Query builder for fluent API
 */
interface QueryBuilder {
  where: (field: string) => OperatorBuilder;
  and: (field: string) => OperatorBuilder;
  or: (field: string) => OperatorBuilder;
  build: () => QueryExpression;
  execute: <T extends Record<string, unknown>>(data: T[]) => T[];
}

interface OperatorBuilder {
  equals: (value: unknown) => QueryBuilder;
  notEquals: (value: unknown) => QueryBuilder;
  greaterThan: (value: number) => QueryBuilder;
  lessThan: (value: number) => QueryBuilder;
  contains: (value: string) => QueryBuilder;
  startsWith: (value: string) => QueryBuilder;
}

const createQueryBuilder = (): QueryBuilder => {
  let expression: QueryExpression | null = null;
  let pendingOperator: "AND" | "OR" | null = null;

  const addExpression = (newExpr: QueryExpression) => {
    if (!expression) {
      expression = newExpr;
    } else if (pendingOperator === "AND") {
      expression = and(expression, newExpr);
    } else if (pendingOperator === "OR") {
      expression = or(expression, newExpr);
    }
    pendingOperator = null;
  };

  const createOperatorBuilder = (field: string): OperatorBuilder => ({
    equals: (value) => {
      addExpression(createComparison(field, "=", value));
      return builder;
    },
    notEquals: (value) => {
      addExpression(createComparison(field, "!=", value));
      return builder;
    },
    greaterThan: (value) => {
      addExpression(createComparison(field, ">", value));
      return builder;
    },
    lessThan: (value) => {
      addExpression(createComparison(field, "<", value));
      return builder;
    },
    contains: (value) => {
      addExpression(createComparison(field, "contains", value));
      return builder;
    },
    startsWith: (value) => {
      addExpression(createComparison(field, "startsWith", value));
      return builder;
    },
  });

  const builder: QueryBuilder = {
    where: (field) => createOperatorBuilder(field),
    and: (field) => {
      pendingOperator = "AND";
      return createOperatorBuilder(field);
    },
    or: (field) => {
      pendingOperator = "OR";
      return createOperatorBuilder(field);
    },
    build: () => expression || createComparison("true", "=", true),
    execute: (data) => {
      const query = builder.build();
      return data.filter(item => query.match({ data: item }));
    },
  };

  return builder;
};

// Usage
interface User {
  name: string;
  age: number;
  role: string;
  email: string;
  active: boolean;
  [key: string]: unknown; // Index signature for Record compatibility
}

const users: User[] = [
  { name: "Alice", age: 30, role: "admin", email: "alice@example.com", active: true },
  { name: "Bob", age: 25, role: "user", email: "bob@test.com", active: true },
  { name: "Charlie", age: 35, role: "admin", email: "charlie@example.com", active: false },
  { name: "Diana", age: 28, role: "user", email: "diana@example.com", active: true },
];

// Query: role = "admin" AND active = true
const activeAdmins = createQueryBuilder()
  .where("role").equals("admin")
  .and("active").equals(true)
  .execute(users);

console.log("Active admins:", activeAdmins.map(u => u.name));

// Query: age > 25 OR email contains "test"
const filtered = createQueryBuilder()
  .where("age").greaterThan(25)
  .or("email").contains("test")
  .execute(users);

console.log("Age > 25 or test email:", filtered.map(u => u.name));
/**
 * Simple template engine
 * Syntax: {{ variable }} for interpolation
 *         {% if condition %}...{% endif %} for conditionals
 *         {% for item in array %}...{% endfor %} for loops
 */

interface TemplateContext {
  data: Record<string, unknown>;
  get: (path: string) => unknown;
}

const createTemplateContext = (data: Record<string, unknown>): TemplateContext => ({
  data,
  get(path) {
    return path.split(".").reduce<unknown>((obj, key) => {
      if (obj && typeof obj === "object") {
        return (obj as Record<string, unknown>)[key];
      }
      return undefined;
    }, data);
  },
});

/**
 * Template node interface
 */
interface TemplateNode {
  render: (context: TemplateContext) => string;
}

/**
 * Text node - literal text
 */
const createTextNode = (text: string): TemplateNode => ({
  render: () => text,
});

/**
 * Variable node - {{ variable }}
 */
const createVariableNode = (path: string): TemplateNode => ({
  render: (context) => String(context.get(path.trim()) ?? ""),
});

/**
 * If node - {% if condition %}...{% endif %}
 */
const createIfNode = (
  condition: string,
  thenBranch: TemplateNode[],
  elseBranch: TemplateNode[] = []
): TemplateNode => ({
  render(context) {
    const value = context.get(condition.trim());
    const nodes = value ? thenBranch : elseBranch;
    return nodes.map(node => node.render(context)).join("");
  },
});

/**
 * For node - {% for item in array %}...{% endfor %}
 */
const createForNode = (
  itemName: string,
  arrayPath: string,
  body: TemplateNode[]
): TemplateNode => ({
  render(context) {
    const array = context.get(arrayPath.trim());
    if (!Array.isArray(array)) return "";

    return array.map((item, index) => {
      const loopContext = createTemplateContext({
        ...context.data,
        [itemName]: item,
        loop: { index, first: index === 0, last: index === array.length - 1 },
      });
      return body.map(node => node.render(loopContext)).join("");
    }).join("");
  },
});

/**
 * Composite node - sequence of nodes
 */
const createCompositeNode = (nodes: TemplateNode[]): TemplateNode => ({
  render: (context) => nodes.map(node => node.render(context)).join(""),
});

/**
 * Template parser (simplified)
 */
const parseTemplate = (template: string): TemplateNode => {
  const nodes: TemplateNode[] = [];
  let remaining = template;

  const patterns = {
    variable: /\{\{\s*([^}]+)\s*\}\}/,
    ifStart: /\{%\s*if\s+([^%]+)\s*%\}/,
    else: /\{%\s*else\s*%\}/,
    endif: /\{%\s*endif\s*%\}/,
    forStart: /\{%\s*for\s+(\w+)\s+in\s+([^%]+)\s*%\}/,
    endfor: /\{%\s*endfor\s*%\}/,
  };

  while (remaining.length > 0) {
    // Find next tag
    const variableMatch = remaining.match(patterns.variable);
    const ifMatch = remaining.match(patterns.ifStart);
    const forMatch = remaining.match(patterns.forStart);

    // Find earliest match
    const matches = [
      { type: "variable" as const, match: variableMatch },
      { type: "if" as const, match: ifMatch },
      { type: "for" as const, match: forMatch },
    ].filter(m => m.match !== null)
     .sort((a, b) => a.match!.index! - b.match!.index!);

    if (matches.length === 0) {
      // No more tags, rest is text
      nodes.push(createTextNode(remaining));
      break;
    }

    const { type, match } = matches[0];
    const index = match!.index!;

    // Add text before tag
    if (index > 0) {
      nodes.push(createTextNode(remaining.slice(0, index)));
    }

    if (type === "variable") {
      nodes.push(createVariableNode(match![1]));
      remaining = remaining.slice(index + match![0].length);
    } else if (type === "if") {
      const condition = match![1];
      remaining = remaining.slice(index + match![0].length);
      
      // Find endif (simplified - doesn't handle nested)
      const endifMatch = remaining.match(patterns.endif);
      const elseMatch = remaining.match(patterns.else);
      
      if (endifMatch) {
        let thenContent: string;
        let elseContent = "";
        
        if (elseMatch && elseMatch.index! < endifMatch.index!) {
          thenContent = remaining.slice(0, elseMatch.index);
          elseContent = remaining.slice(
            elseMatch.index! + elseMatch[0].length,
            endifMatch.index
          );
        } else {
          thenContent = remaining.slice(0, endifMatch.index);
        }
        
        const thenNodes = [parseTemplate(thenContent)];
        const elseNodes = elseContent ? [parseTemplate(elseContent)] : [];
        
        nodes.push(createIfNode(condition, thenNodes, elseNodes));
        remaining = remaining.slice(endifMatch.index! + endifMatch[0].length);
      }
    } else if (type === "for") {
      const itemName = match![1];
      const arrayPath = match![2];
      remaining = remaining.slice(index + match![0].length);
      
      // Find endfor
      const endforMatch = remaining.match(patterns.endfor);
      if (endforMatch) {
        const bodyContent = remaining.slice(0, endforMatch.index);
        const bodyNodes = [parseTemplate(bodyContent)];
        
        nodes.push(createForNode(itemName, arrayPath, bodyNodes));
        remaining = remaining.slice(endforMatch.index! + endforMatch[0].length);
      }
    }
  }

  return createCompositeNode(nodes);
};

/**
 * Template engine
 */
interface TemplateEngine {
  compile: (template: string) => (data: Record<string, unknown>) => string;
  render: (template: string, data: Record<string, unknown>) => string;
}

const createTemplateEngine = (): TemplateEngine => ({
  compile(template) {
    const ast = parseTemplate(template);
    return (data) => ast.render(createTemplateContext(data));
  },
  render(template, data) {
    return this.compile(template)(data);
  },
});

// Usage
const engine = createTemplateEngine();

const template = `
Hello, {{ user.name }}!

{% if user.isAdmin %}
You have admin privileges.
{% else %}
You are a regular user.
{% endif %}

Your items:
{% for item in items %}
- {{ item.name }}: ${{ item.price }}
{% endfor %}

Total: ${{ total }}
`;

const data = {
  user: { name: "Alice", isAdmin: true },
  items: [
    { name: "Widget", price: "9.99" },
    { name: "Gadget", price: "19.99" },
    { name: "Gizmo", price: "14.99" },
  ],
  total: "44.97",
};

const output = engine.render(template, data);
console.log(output);

When to Use


Real-World Applications

ApplicationUsage
SQLQuery interpretation
Regular ExpressionsPattern matching
Template EnginesHandlebars, Mustache
Config FilesYAML, JSON, TOML parsing
SpreadsheetsFormula evaluation
Game EnginesScripting languages

Summary

Key Takeaway: Interpreter defines a grammar and interprets sentences in that language. Best for simple DSLs, expression evaluation, and configuration parsing.

Pros

  • ✅ Easy to change and extend grammar
  • ✅ Implementing grammar is straightforward
  • ✅ Good for simple languages

Cons

  • ❌ Complex grammars are hard to maintain
  • ❌ Can be slow for complex expressions
  • ❌ Requires many classes for grammar rules

On this page