Skip to main content

The Builder Pattern In TypeScript

The Builder pattern is a creational design pattern that lets you construct complex objects step by step.

Director

The Director is responsible for executing the building steps in a particular sequence. It's not responsible for the product, it only guides the construction process based on the type of product it's told to create. It works with a builder instance which it controls.

In our example, the Director has two methods for building a product: buildMinimalViableProduct and buildFullFeaturedProduct. The first method builds a product with minimum features (it calls the setPartA method of the builder), and the second method builds a fully featured product (it calls setPartA, setPartB, and setPartC methods of the builder).

The director class is optional, as the client code can control the builders directly. However, the Director class can be a good place to put construction code that produces a certain kind of product, especially if this code involves complex sequences of construction steps.

ConcreteBuilder1

ConcreteBuilder1 is a class that implements the Builder interface. It provides an implementation for all the construction steps defined in the interface. Each step in this class is responsible for a part of product construction, like building a part of a complex object. These steps can be called multiple times, or in a different sequence, to create different representations of a product.

The ConcreteBuilder1 also maintains a reference to the product. When the product is finished, the client code retrieves the result from the builder. The builder can then be ready to start producing another product. That's why it's a usual practice to call the reset method at the end of getProduct.

In our case, the ConcreteBuilder1 is responsible for creating and assembling the Product1. It provides methods for adding parts (setPartA, setPartB, setPartC) to the product.

So, in summary: the Director determines the order in which to call construction steps, and the ConcreteBuilder1 is responsible for the actual creation and assembly of product parts.

TypeScript Course Instructor Image
TypeScript Course Instructor Image

Time To Transition From JavaScript To TypeScript

Level Up Your TypeScript And Object Oriented Programming Skills. The only complete TypeScript course on the marketplace you building TypeScript apps like a PRO.

SEE COURSE DETAILS

Classic Implementation

interface Builder {
setPartA(): void;

setPartB(): void;

setPartC(): void;
}

class ConcreteBuilder1 implements Builder {
private product!: Product1;

constructor() {
this.reset();
}

public reset(): void {
this.product = new Product1();
}

public setPartA(): void {
this.product.add("PartA1");
}

public setPartB(): void {
this.product.add("PartB1");
}

public setPartC(): void {
this.product.add("PartC1");
}

public getProduct(): Product1 {
const result = this.product;
this.reset();
return result;
}
}

class Product1 {
private parts: string[] = [];

public add(part: string): void {
this.parts.push(part);
}

public listParts(): void {
console.log(`Product parts: ${this.parts.join(", ")}`);
}
}

class Director {
private builder!: Builder;

public setBuilder(builder: Builder): void {
this.builder = builder;
}

public buildMinimalViableProduct(): void {
this.builder.setPartA();
}

public buildFullFeaturedProduct(): void {
this.builder.setPartA();
this.builder.setPartB();
this.builder.setPartC();
}
}

function clientCode(director: Director) {
const builder = new ConcreteBuilder1();
director.setBuilder(builder);

console.log("Standard basic product:");
director.buildMinimalViableProduct();
builder.getProduct().listParts();

console.log("Standard full featured product:");
director.buildFullFeaturedProduct();
builder.getProduct().listParts();

// Remember, the Builder pattern can be used without a Director class.
console.log("Custom product:");
builder.setPartA();
builder.setPartB();
builder.getProduct().listParts();
}

const director = new Director();
clientCode(director);

In this example, we have a Builder interface that defines the general methods for constructing a product. ConcreteBuilder1 is a class that implements this interface, constructing and assembling parts of the product by implementing the Builder methods.

Product1 is the product class, where each product consists of multiple parts. The add() method adds a part to the product.

Director is the class that orchestrates the construction process. It has methods for building a minimal viable product and a full-featured product. The director calls the appropriate methods of the builder to construct the product.

The clientCode() function demonstrates how the builder pattern is used. The director and builder work together to construct different variants of the product.

This approach isolates the construction code from the object representation code, making it easier to handle complex objects, manage the construction process, and extend it to construct different representations.

Issues With Property Initialization In TypeScript

You will see that if we will encounter two errors with the above implementation

Property product has no initializer and is not definitely assigned in the constructor. Property builder has no initializer and is not definitely assigned in the constructor.

The errors we are encountering are because we are using the --strictPropertyInitialization flag (or --strict which includes it) with TypeScript, and the product property in ConcreteBuilder1 class and builder property in Director class are not definitely assigned in their constructors.

The --strictPropertyInitialization flag makes sure that every instance property of a class gets a value in the constructor body, or by a property initializer.

There are a few ways to resolve this issue. One way is to provide a value in the constructor for these properties. Another way is to tell TypeScript that these values will be definitely assigned later using the ! (non-null assertion operator). This is what we have done in the following code:

class ConcreteBuilder1 implements Builder {
private product!: Product1;

constructor() {
this.reset();
}

public reset(): void {
this.product = new Product1();
}

// rest of your code...
}

class Director {
private builder!: Builder;

public setBuilder(builder: Builder): void {
this.builder = builder;
}

// rest of the code
}

In the above code, the ! operator tells TypeScript that these values will be definitely assigned later, so TypeScript will not show an error. Please note that this approach should be used with care, because if the values are not actually assigned later and you try to use them, this could lead to runtime errors. It's best to ensure these values are definitely assigned before they are used.

When To Use The Builder Pattern

The Builder Pattern is typically used to construct a complex object step by step and the same construction process can create different types of objects.

Here are some signs that might indicate that a Builder pattern could be appropriate:

  1. Complex Object Creation: If your software needs to create complex objects that have many attributes, some of which are optional and some are mandatory, the Builder pattern can simplify this process and make your code more readable.

  2. Step-by-step Object Creation: If an object must be created in multiple steps, especially if these steps need to be executed in a specific order, the Builder pattern can be a good fit. It provides a way to ensure the object is always constructed in a valid state.

  3. Combination Explosion: If you are dealing with an object that can be configured in many different ways (such that attempting to provide a constructor for every combination of configurations would be impractical), the Builder pattern can be useful. This is sometimes referred to as the " telescoping constructor" problem.

  4. Constructing Composite Structures: If you need to construct a composite or hierarchical structure (like a tree), a builder can make it easier to understand and maintain the code.

  5. Immutable Objects: If you want to construct an immutable object with many attributes, the Builder pattern can be used to construct the object in steps, and then deliver the final, immutable object.

  6. Code Clarity: If you have a constructor with many parameters and it's not clear what each parameter is for (because they have the same type or aren't self-explanatory), using the Builder pattern can improve code readability. It allows you to set the name of each attribute individually when creating an object.

Remember, design patterns should be used judiciously and where they add value. While the Builder pattern can help in the situations described above, in simpler cases, it might just overcomplicate your code.

What Can You Do Next 🙏😊

If you liked the article, consider subscribing to Cloudaffle, my YouTube Channel, where I keep posting in-depth tutorials and all edutainment stuff for ssoftware developers.

YouTube @cloudaffle