Photo by Nazarii Yurkov on Unsplash
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