Builder pattern on steroïds with generics and proxy in Typescript

or How the power of Typescript generics and Javascript proxies can allow developers to get ride of a huge number of bolier plate code lines [1/6]

Context

Using an hexagonal or clean architecture is a good way to have a back-end centerred on the business value provided by the application it serves while making it robust on technological changes & evolutions

The business entities are at the core of this architecture. Those entities reflect the different concepts that are manipulated by the back-end of the application. As such another common good practice is to have builders classes to create those entities. As their name indicates, those builder classes follow the Builder Design Pattern.

When you application grows, the number of your business entities can grows quite quickly. As such having a common infrastructure for all those builders of business entities make the creation process less cumbersome.

Using typescript and the power of generics, we will see how one can create a base class providing a lot of type checking feature at build time and runtime compatibility that removes the needs of code duplication.

The main goal of this group articles is to showcase some of the latest typescript feature will providing a useful helper that you can use in your projets.

Use case

Imagine I want to create an App for storing cooking recipes. I will start simple with at 2 business entities : one that represents a recipe and another one that represents an ingredient of the recipe. A ingredient will have some properties such as its kind, its amount in weight and a name. A recipe will have some properties too : a difficulty, a list of ingredients and a name.

I will need also builders for those two entities in order to conveniently build each one of those entities.

In typescript, this translate into code with :

Ingredient Business Entity
export default class IngredientBE {
  public kind: EKind
  public weightInKg?: string
  public name: string
}

export enum EKind {
  Liquid = 0,
  Vegetable = 1,
  Fruit = 2,
  Meat = 3,
  Condiment = 4,
  Other = 5,
}

Recipe Business Entity

export default class Recipe {
  public ingredients: Ingredient[]
  public difficulty: EDifficulty
  public name: string
}

export enum EDifficulty{
  Easy = 0,
  Medium = 1,
  Hard = 2
}

Ingredient builder

import Ingredient, { EKind } from 'src/useCases/business/entities/Ingredient'

export default class IngredientBuilder {

  private builtEntity: Ingredient

  constructor() {
    this.builtEntity = new Ingredient()
  }

  public static anIngredient() {
    return new IngredientBuilder(Ingredient)
  }
  public withWeightInGrams(weightInGrams?: number) {
    this.builtEntity.weightInGrams =  weightInGrams
    return this
  }

  public withKind(kind: EKind) {
    this.builtEntity.kind = kind
    return this
  }
  public withName(name: string) {
    this.builtEntity.name = name
    return this
  }

  public build(): Ingredient {
    const returnedObj = this.builtEntity
    this.builtEntity = new Ingredient()
    return returnedObj
  }
}

Recipe builder

import Ingredient from 'src/useCases/business/entities/Ingredient'
import Recipe, { EDifficulty } from 'src/useCases/business/entities/Recipe'

export default class RecipeBuilder {

  private builtEntity: Recipe

  constructor() {
    this.builtEntity = new Recipe()
  }

  public static anRecipe() {
    return new RecipeBuilder(Recipe)
  }
  public withDifficulty(difficulty: EDifficulty) {
    this.builtEntity.difficulty = difficulty
    return this
  }

  public withName(name: string) {
    this.builtEntity.name = name
    return this
  }

  public withIngredients(ingredients: Ingredient[]) {
    this.builtEntity.ingredients = ingredients
    return this
  }

  public build(): Recipe {
    const returnedObj = this.builtEntity
    this.builtEntity = new Recipe()
    return returnedObj
  }
}

Up next

From this simple data models as a starting point, I will take a very didactic approach to explain the different feature of typescript we can use to dramatically simplify this code.

If you are already familiar with advanced typescript features and just want to see what could be a convenient-to-use builder infrastructure that makes good use of those features, please have a look at this repo : berlingo-ts