Lab 14: Advanced Patterns

Objective

Implement production TypeScript patterns: Builder, Observer, Command, Strategy, Repository with generics, type-safe event emitter, and the Fluent Builder pattern.

Time

35 minutes

Prerequisites

  • Lab 04 (Classes), Lab 06 (Generics)

Tools

  • Docker image: zchencow/innozverse-ts:latest


Lab Instructions

Step 1: Fluent Builder Pattern

// Immutable fluent builder using generics
class QueryBuilder<T extends Record<string, unknown>> {
    private conditions: string[] = [];
    private _limit?: number;
    private _offset?: number;
    private _orderBy?: { field: keyof T; direction: "ASC" | "DESC" };
    private _fields?: (keyof T)[];

    constructor(private table: string) {}

    select(...fields: (keyof T)[]): this {
        this._fields = fields;
        return this;
    }

    where(field: keyof T, op: "=" | ">" | "<" | ">=" | "<=" | "LIKE", value: unknown): this {
        this.conditions.push(`${String(field)} ${op} ${JSON.stringify(value)}`);
        return this;
    }

    limit(n: number): this { this._limit = n; return this; }
    offset(n: number): this { this._offset = n; return this; }

    orderBy(field: keyof T, direction: "ASC" | "DESC" = "ASC"): this {
        this._orderBy = { field, direction };
        return this;
    }

    build(): string {
        const fields = this._fields?.map(String).join(", ") ?? "*";
        let sql = `SELECT ${fields} FROM ${this.table}`;
        if (this.conditions.length) sql += ` WHERE ${this.conditions.join(" AND ")}`;
        if (this._orderBy) sql += ` ORDER BY ${String(this._orderBy.field)} ${this._orderBy.direction}`;
        if (this._limit)  sql += ` LIMIT ${this._limit}`;
        if (this._offset) sql += ` OFFSET ${this._offset}`;
        return sql;
    }
}

interface Product { id: number; name: string; price: number; category: string; stock: number; }

const query = new QueryBuilder<Product>("products")
    .select("id", "name", "price")
    .where("category", "=", "Laptop")
    .where("price", "<", 1000)
    .orderBy("price", "DESC")
    .limit(10)
    .offset(0)
    .build();

console.log("SQL:", query);

💡 keyof T in Builder ensures you can only reference actual properties of the target type. where("invalid_column", ...) would be a compile error. This makes the query builder safe — misspelled column names are caught at development time, not at runtime when the query fails.

📸 Verified Output:


Step 2: Observer / Event Emitter Pattern

💡 Returning an unsubscribe function from on() is the modern pattern (React hooks use this). The alternative is emitter.off(event, listener) but it requires keeping a reference to the exact function. The returned () => void closure captures the reference, so callers don't need to.

📸 Verified Output:


Steps 3–8: Command, Strategy, Repository, Proxy, State Machine, Capstone

📸 Verified Output:


Summary

TypeScript patterns elevate code quality dramatically. You've implemented a type-safe fluent QueryBuilder, typed EventEmitter with unsubscribe, Command pattern with undo/redo, Strategy pattern, Specification pattern for filtering, logging Proxy, and a typed state machine. These patterns are in Angular, NestJS, RxJS, and every large TypeScript codebase.

Further Reading

Last updated