RizTech Academy logo
RizTech Academy
Design Patterns in TypeScriptLesson 3 of 530 min

Factory and builder with types and generics

Two creational patterns — Factory and Builder — are about how objects are made. In TypeScript, the Factory survives as a genuinely useful function, and the Builder mostly dissolves into an object literal with optional properties. This lesson shows both, and where generics make the factory pattern shine.

Factory — a function that creates

The Factory pattern's problem: a plain constructor is limited — it cannot have a descriptive name, choose a subtype, return a cached instance, or validate cleanly. A factory is a function whose job is to create an object, with more freedom than a new.

In TypeScript, a factory is just a function — no "factory class" needed:

type Notification =
  | { type: "email"; to: string }
  | { type: "sms"; phone: string };

// a factory: chooses the variant and names the intent
function createNotification(contact: string): Notification {
  return contact.includes("@")
    ? { type: "email", to: contact }
    : { type: "sms", phone: contact };
}

createNotification("kavita@example.com");   // { type: "email", ... }
createNotification("9876500001");           // { type: "sms", ... }

createNotification reads better than a constructor and does what a constructor cannot — choose the variant from the input. Factories also shine for named creation where a bare constructor argument is ambiguous:

type Duration = { ms: number };
const Duration = {
  fromSeconds: (s: number): Duration => ({ ms: s * 1000 }),
  fromMinutes: (m: number): Duration => ({ ms: m * 60 * 1000 }),
};

Duration.fromMinutes(5);   // clearer than a constructor whose unit you cannot see

This "factory functions grouped in an object" is idiomatic TypeScript — clearer than a class with static methods for simple cases. The Factory pattern genuinely earns its place in TypeScript — as a function.

Generic factories — where they get powerful

Factories combine beautifully with generics to build typed things:

function createStore<T>(initial: T) {
  let state = initial;
  return {
    get: (): T => state,
    set: (next: T): void => { state = next; },
  };
}

const counter = createStore(0);       // T inferred number
counter.get();                        // number
counter.set(5);
counter.set("five");                  // error — T is number

const nameStore = createStore("Kavita");   // T inferred string

createStore<T> is a generic factory — it creates a typed store for any value type, inferring T from the initial value. The returned object's methods are fully typed. This "a factory function returning a typed object of closures" is a common, powerful TypeScript pattern (it is roughly how React's useState and many small state utilities work), and it needs no class at all — a function and a closure do it, with full type inference.

Builder — mostly replaced by object literals

The Builder pattern's problem: constructing an object with many optional parameters. In Java, a ten-parameter constructor is unusable, so the Builder — a helper with a method per field and a final build() — was invented. In TypeScript, an object literal with optional properties (or a config object) does this directly:

// the Java-style Builder — unnecessary in TypeScript
// new Pizza.Builder().size("large").cheese(true).build()

// TypeScript — an options object with optional/defaulted fields:
interface PizzaOptions {
  size: "small" | "medium" | "large";
  cheese?: boolean;
  toppings?: string[];
}

function createPizza(options: PizzaOptions) {
  const { size, cheese = true, toppings = [] } = options;
  return { size, cheese, toppings };
}

createPizza({ size: "large", toppings: ["mushroom"] });   // named, defaults applied, no builder

An options object with optional properties, destructured with defaults, gives you exactly what the Builder provided — each field named, unwanted ones defaulted — with none of the machinery. TypeScript's object literals are the builder. When you have optional properties and destructuring defaults, you almost never need a Builder class.

When a builder (or fluent API) still helps

Two cases where builder-like machinery earns its place, even in TypeScript:

A genuinely complex, staged, or conditional construction — building a query, a form, or a pipeline up across many steps, sometimes conditionally (add a filter only if a value is present). A fluent builder (chained methods returning this or a new builder) can read well here:

const query = new QueryBuilder()
  .from("users")
  .where("age", ">", 18)
  .orderBy("name")
  .build();

Query builders (like Prisma's or Knex's) and validation schemas (Zod's z.object({...}).partial()) use fluent, chainable APIs — and TypeScript's generics can make each step change the type (a builder that tracks what has been set), which is a genuinely powerful, type-safe use of the pattern. This is the one place builders shine in TypeScript.

A required-then-optional construction where the type system should enforce order — advanced, using generics to track builder state, and worth it only for a library-grade API.

For ordinary object construction, though, an options object wins — reach for a builder only when construction is genuinely complex or staged.

The scoreboard

Pattern Java TypeScript
Factory Often a factory class A function (or factory functions in an object) — still useful, great with generics
Builder A builder class per object An options object with optional/defaulted fields — rarely a builder
Fluent builder — Earns its place for complex/staged construction (query builders, schemas)

Check your work

What a Factory solves. A constructor's limits — a factory function can name the intent, choose a variant, cache, or validate.

How a Factory is written in TypeScript. As a function (or factory functions grouped in an object) — no factory class.

Where generic factories shine. Creating a typed thing for any type — createStore<T> returns a typed object of closures, T inferred from the input.

What Builder solves, and why TypeScript rarely needs it. Constructing objects with many optional parameters; an options object with optional/defaulted properties does the same directly.

How an options object replaces a builder. Optional properties + destructuring defaults give named, defaulted construction with no machinery.

When a fluent builder still helps. Genuinely complex, staged, or conditional construction — query builders, schemas — where generics can even make each step change the type.

The scoreboard. Factory → a function (useful); Builder → an options object (usually); fluent builder → complex/staged construction only.

Practice

  1. Write a createNotification factory that returns a discriminated-union variant based on the input. Test both branches.
  2. Group factory functions in an object (Duration.fromSeconds, fromMinutes) and use them.
  3. Write a generic createStore<T> and confirm it infers T and rejects a wrong-typed set.
  4. Replace an imagined Builder with an options-object function using optional properties and destructuring defaults. Construct one, omitting the defaulted fields.
  5. Sketch a fluent QueryBuilder with chained methods returning this, and note where it reads better than an options object.
  6. For three "how do I construct this?" cases, decide factory function, options object, or fluent builder, and justify each.

Official documentation

Next: dependency injection and decorators — the pattern that earns its place.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship