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
- Write a
createNotificationfactory that returns a discriminated-union variant based on the input. Test both branches. - Group factory functions in an object (
Duration.fromSeconds,fromMinutes) and use them. - Write a generic
createStore<T>and confirm it infersTand rejects a wrong-typedset. - Replace an imagined Builder with an options-object function using optional properties and destructuring defaults. Construct one, omitting the defaulted fields.
- Sketch a fluent
QueryBuilderwith chained methods returningthis, and note where it reads better than an options object. - For three "how do I construct this?" cases, decide factory function, options object, or fluent builder, and justify each.
Official documentation
- TypeScript — Functions — Factory functions.
- TypeScript — Generics — For generic factories.
- TypeScript — Object destructuring with defaults — The builder replacement.
- Refactoring Guru — Factory Method and Builder — The patterns, to implement the TypeScript way.
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