The type-safe builder pattern helps you create objects step-by-step while making sure you don't miss any required parts. It uses TypeScript's types to catch mistakes before running the code.
Type-safe builder pattern in Typescript
class Builder<Step = Initial> { private data: Partial<MyObject> = {}; setPartA(value: string): Builder<NextStep> { this.data.partA = value; return this as unknown as Builder<NextStep>; } setPartB(value: number): Builder<FinalStep> { this.data.partB = value; return this as unknown as Builder<FinalStep>; } build(this: Builder<FinalStep>): MyObject { return this.data as MyObject; } }
Use generic types to track which parts are set.
The build method only works when all required parts are set.
Car object. You must set make, model, and year before calling build.interface Car {
make: string;
model: string;
year: number;
}
class CarBuilder<MakeSet = false, ModelSet = false, YearSet = false> {
private car: Partial<Car> = {};
setMake(make: string): CarBuilder<true, ModelSet, YearSet> {
this.car.make = make;
return this as any;
}
setModel(model: string): CarBuilder<MakeSet, true, YearSet> {
this.car.model = model;
return this as any;
}
setYear(year: number): CarBuilder<MakeSet, ModelSet, true> {
this.car.year = year;
return this as any;
}
build(this: CarBuilder<true, true, true>): Car {
return this.car as Car;
}
}const car = new CarBuilder() .setMake('Toyota') .setModel('Corolla') .setYear(2023) .build();
This program builds a pizza object step-by-step. It requires setting the size and cheese before building. Pepperoni is optional.
interface Pizza {
size: string;
cheese: boolean;
pepperoni: boolean;
}
class PizzaBuilder<SizeSet = false, CheeseSet = false> {
private pizza: Partial<Pizza> = {};
setSize(size: string): PizzaBuilder<true, CheeseSet> {
this.pizza.size = size;
return this as any;
}
addCheese(cheese: boolean): PizzaBuilder<SizeSet, true> {
this.pizza.cheese = cheese;
return this as any;
}
addPepperoni(pepperoni: boolean): PizzaBuilder<SizeSet, CheeseSet> {
this.pizza.pepperoni = pepperoni;
return this as any;
}
build(this: PizzaBuilder<true, true>): Pizza {
return this.pizza as Pizza;
}
}
const myPizza = new PizzaBuilder()
.setSize('Large')
.addCheese(true)
.addPepperoni(true)
.build();
console.log(myPizza);TypeScript's type system helps catch missing required steps before running the code.
Use Partial<T> to build objects gradually.
Returning this as any is a common trick to change builder state types.
The type-safe builder pattern helps create objects safely step-by-step.
It uses TypeScript generics to track which parts are set.
This prevents mistakes like missing required properties when building objects.
Practice
What is the main benefit of using a type-safe builder pattern in TypeScript?
Solution
Step 1: Understand the purpose of type-safe builder pattern
The pattern helps build objects step-by-step while ensuring correctness.Step 2: Identify the key benefit
It uses TypeScript's type system to check that all required properties are set before final object creation.Final Answer:
It ensures all required properties are set before creating the object. -> Option AQuick Check:
Type safety = required properties checked [OK]
- Thinking it improves runtime speed
- Confusing it with UI generation
- Assuming it removes interfaces
Which of the following is the correct way to declare a generic builder class in TypeScript?
class Builder<T> {
private data: T;
constructor() {
this.data = {} as T;
}
build(): T {
return this.data;
}
}Solution
Step 1: Check generic class declaration
class Builder<T> { private data: T; constructor() { this.data = {} as T; } build(): T { return this.data; } } correctly declares a generic class with<T>and initializesdataas{} as T.Step 2: Verify build method return type
Thebuild()method returnsdatatyped asT, matching the generic type.Final Answer:
class Builder<T> { private data: T; constructor() { this.data = {} as T; } build(): T { return this.data; } } -> Option CQuick Check:
Generic class with typed data = class Builder<T> { private data: T; constructor() { this.data = {} as T; } build(): T { return this.data; } } [OK]
<T> and proper initialization [OK]- Using any instead of generic type
- Initializing data with null or undefined
- Missing generic parameter in class
Consider this TypeScript code using a type-safe builder pattern:
interface User {
name: string;
age: number;
}
class UserBuilder {
private user = {} as Partial<User>;
setName(name: string) { this.user.name = name; return this; }
setAge(age: number) { this.user.age = age; return this; }
build(): User {
if (!this.user.name || !this.user.age) throw new Error('Missing fields');
return this.user as User;
}
}
const user = new UserBuilder().setName('Alice').setAge(30).build();
console.log(user);What will be the output?
Solution
Step 1: Trace method calls on UserBuilder
The builder setsnameto 'Alice' andageto 30, so both required fields are set.Step 2: Check build method behavior
Since both fields exist, no error is thrown, and the object is returned asUser.Final Answer:
{ name: 'Alice', age: 30 } -> Option AQuick Check:
All fields set = object returned [OK]
- Assuming error thrown even when fields set
- Expecting undefined output
- Missing age property in output
Identify the error in this type-safe builder pattern code and select the fix:
interface Car {
make: string;
year: number;
}
class CarBuilder {
private car = {} as Partial<Car>;
setMake(make: string) { this.car.make = make; return this; }
setYear(year: number) { this.car.year = year; return this; }
build(): Car {
if (!this.car.make || !this.car.year) throw new Error('Missing fields');
return this.car;
}
}Solution
Step 1: Identify the type mismatch in build()
The method returnsthis.carwhich is typed asPartial<Car>, but the return type isCar.Step 2: Fix by casting to Car
To satisfy the return type, castthis.carasCarafter checking required fields.Final Answer:
Cast return value as Car: return this.car as Car; -> Option DQuick Check:
Return type must match method signature [OK]
- Changing return type to Partial<Car>
- Removing validation checks
- Initializing with full Car without setting fields
You want to create a type-safe builder for a Product with required id and name, and optional description. Which approach best enforces setting required fields before build?
interface Product {
id: number;
name: string;
description?: string;
}
// Which builder pattern enforces required fields at compile time?Solution
Step 1: Understand compile-time enforcement
Only generics with flags and conditional types can enforce required fields before build at compile time.Step 2: Compare options
Use generics with flags tracking set fields and conditional types for build() uses TypeScript generics and conditional types to track which fields are set, preventing build() call until required fields are set.Final Answer:
Use generics with flags tracking set fields and conditional types for build() -> Option BQuick Check:
Compile-time safety = generics + conditional types [OK]
- Relying only on runtime checks
- Using inheritance without generics
- Returning partial objects without enforcement
Ready to prove it?
Pass the test and this pattern moves unknown
