Chapter 10.4☕ 16 min read

Builder Pattern in TS

Build complex objects step by step — each method returns this for a clean fluent API.

01Step-by-Step Biryani Order

Some objects are simple — a few parameters in a constructor and you're done. But many real-world objects have multiple optional fields, complex validation rules, and interdependent settings. Passing 10 parameters to a constructor is a nightmare — you have to remember the order, skip nulls for optional fields, and the code is completely unreadable. This is where the Builder pattern comes in.

Think of ordering biryani at a famous Hyderabadi restaurant like Paradise or Bawarchi. You don't just say "one biryani" — you build your order step by step:

  1. First, you select the base: Chicken, Mutton, or Egg? (this is your first builder method)
  2. Then, you choose the spice level: Mild, Medium, or Spicy? (another method)
  3. Then the rice type: Basmati or Jeera Rice? (another method)
  4. Then extras: Extra gravy, boiled egg, raita? (methods you can call multiple times)
  5. Finally, the quantity: How many plates? (one last method)

The waiter doesn't ask you all these questions at once — they guide you through the process step by step. Each answer leads to the next question. This is EXACTLY how the Builder pattern works. Each method call guides you to the next step. The builder accumulates state, and when you're done, you call .build() to get your final biryani order — fully constructed, validated, and ready to go.

In TypeScript, the Builder pattern is especially powerful because you can type each step, making the chain self-documenting and impossible to misuse. TypeScript will tell you if you forgot a required step or passed an invalid value.

02The Builder Pattern

The Builder pattern separates the construction of a complex object from its representation. Let's build a typed biryani order builder.

// Step 1: Define the final product
interface BiryaniOrder {
  base: "chicken" | "mutton" | "egg";
  spice: "mild" | "medium" | "spicy";
  rice: "basmati" | "jeera";
  extras: string[];
  quantity: number;
  readonly totalPrice: number;
}

// Step 2: Create the builder class
class BiryaniBuilder {
  private config: Partial<BiryaniOrder> = {
    spice: "medium",
    rice: "basmati",
    extras: [],
    quantity: 1,
  };

  selectBase(
    base: BiryaniOrder["base"]
  ): this {
    this.config.base = base;
    return this;
  }

  withSpice(
    spice: BiryaniOrder["spice"]
  ): this {
    this.config.spice = spice;
    return this;
  }

  withRice(
    rice: BiryaniOrder["rice"]
  ): this {
    this.config.rice = rice;
    return this;
  }

  addExtra(extra: string): this {
    this.config.extras!.push(extra);
    return this;
  }

  setQuantity(qty: number): this {
    this.config.quantity = qty;
    return this;
  }

  build(): BiryaniOrder {
    if (!this.config.base) {
      throw new Error(
        "Base is required!"
      );
    }

    const basePrice =
      this.config.base === "mutton" ? 350
      : this.config.base === "chicken" ? 250
      : 200;

    const order: BiryaniOrder = {
      base: this.config.base,
      spice: this.config.spice!,
      rice: this.config.rice!,
      extras: [...this.config.extras!],
      quantity: this.config.quantity!,
      totalPrice: (
        basePrice + this.config.extras!.length * 30
      ) * this.config.quantity!,
    };

    return Object.freeze(order);
  }
}
03Fluent API with Typed Builders

TypeScript makes the Builder pattern even more powerful with typed fluent APIs. Let's explore advanced patterns.

Generic Builder

class GenericBuilder<T> {
  private obj: Partial<T> = {};

  set<K extends keyof T>(
    key: K,
    value: T[K]
  ): this {
    this.obj[key] = value;
    return this;
  }

  build(): T {
    return { ...this.obj } as T;
  }
}

// Usage
const user = new GenericBuilder<User>()
  .set("name", "Imran")
  .set("age", 25)
  .build();

Step Builder (Type-Safe Ordering)

class StepBuilder {
  static create(): Step1 {
    return new Step1();
  }
}

class Step1 {
  selectBase(
    base: "chicken" | "mutton"
  ): Step2 {
    return new Step2(base);
  }
}

class Step2 {
  constructor(
    private base: string
  ) {}

  withSpice(
    spice: "mild" | "spicy"
  ): Step3 {
    return new Step3(
      this.base, spice
    );
  }
}

// The type system ENFORCES the
// correct order!
StepBuilder.create()
  .selectBase("chicken")
  // Can't skip to extras
  .withSpice("spicy")
  .build();

Fluent API with Interfaces

interface IQueryBuilder<T> {
  where(
    field: keyof T,
    op: string,
    val: unknown
  ): this;
  orderBy(
    field: keyof T,
    dir: "asc" | "desc"
  ): this;
  limit(n: number): this;
  execute(): Promise<T[]>;
}

class QueryBuilder<T>
  implements IQueryBuilder<T> {
  // Implementation with typed
  // field access via keyof
}
04Builder Pattern Traps

Builder pattern has some pitfalls. Let's go through the key ones.

Trap 1: Not Returning this — Break the chain by returning void. Always return this from setter methods.

Trap 2: Mutable build() Output — Returning internal state directly allows external mutation. Return a copy of frozen object.

Trap 3: Skipping Validation — Not validating in build() lets incomplete objects slip through.

Trap 4: Over-Engineering — For simple objects with 2-3 parameters, a builder is overkill. Use optional parameters or default values in the constructor instead.

05Builder Pattern Cheatsheet

Basic Builder Structure:

class Builder {
  private config: Partial<T> = { defaults };
  method(val: Type): this { ...; return this; }
  build(): T { validate; return frozen; }
}

Key Rules:

  • Each setter returns this for method chaining
  • build() validates required fields and returns immutable object
  • Use Partial<T> for internal state with gradual construction
  • Return frozen copies from build() to prevent mutations
  • Consider step builders for complex sequential construction
  • Don't use builders for simple objects — constructors are fine

The Golden Rule: "The Builder pattern is like ordering biryani step by step at Paradise — you don't dump all requirements at once. Each method is a step in the process, each chain is the conversation with the waiter, and build() is the final plate arriving at your table. Keep it fluent, keep it typed, keep it delicious, bhai!"

Key Takeaways

  • Builder pattern separates object construction from representation — step by step
  • Each setter returns this (the builder instance) for method chaining
  • build() validates required fields and returns the final immutable object
  • Use Partial for internal state that is built up gradually
  • Return frozen copies from build() to prevent external mutation
  • Step builders can enforce construction order at the type level
Course Search
Search across all chapters & stages
📖

Search the course

Type any topic — branching, stash, rebase, hooks — and jump straight to that chapter.

merge branchesgit stashundo commitrebase