Constructing complex objects with Builder Pattern
When an object has many properties, passing a long list of parameters to a constructor can become messy and hard to read. This is often known as the “telescoping constructor” anti-pattern.
The Builder Pattern is a creational design pattern that solves this by letting you construct complex objects step by step, improving readability and maintainability.
Instead of initializing directly with a massive constructor:
// Hard to remember what each parameter means!const user = new User("John", "Doe", 30, "john@example.com", "123 Main St", true);Using the Builder Pattern:
class User { constructor( public firstName: string, public lastName: string, public age?: number, public email?: string, public address?: string, public isActive?: boolean ) {}}
class UserBuilder { private user: Partial<User> = {};
// Required fields can be set in the Builder's constructor constructor(firstName: string, lastName: string) { this.user.firstName = firstName; this.user.lastName = lastName; }
setAge(age: number): this { this.user.age = age; return this; // Return 'this' to allow method chaining }
setEmail(email: string): this { this.user.email = email; return this; }
setAddress(address: string): this { this.user.address = address; return this; }
setIsActive(isActive: boolean): this { this.user.isActive = isActive; return this; }
build(): User { // Construct the final object return new User( this.user.firstName!, this.user.lastName!, this.user.age, this.user.email, this.user.address, this.user.isActive ); }}
// Construction is fluent, explicit, and easy to readconst user = new UserBuilder("John", "Doe") .setAge(30) .setEmail("john@example.com") .setAddress("123 Main St") .setIsActive(true) .build();This pattern makes your code more readable, flexible, and provides a clear separation between object construction and its representation.
Real-world Examples
The Builder pattern is extremely common in the TypeScript/JavaScript ecosystem, especially for constructing complex queries or configuration objects. Here are a few popular examples:
-
Knex.js & TypeORM: SQL query builders heavily rely on this pattern to construct complex database queries step by step.
// TypeORM exampleconst user = await dataSource.createQueryBuilder().select("user").from(User, "user").where("user.id = :id", { id: 1 }).getOne(); -
Supertest (with Jest): Used for HTTP assertions, where you build a request step by step.
await request(app).get('/user').set('Accept', 'application/json').expect('Content-Type', /json/).expect(200); -
Mongoose: Querying MongoDB uses a chained builder approach.
User.find().where('age').gt(18).sort('-createdAt').limit(10).exec();