Use of base class to mutualize behavior / use of generic make the typing parametric
Typescript builder tutorial [2/6]

First analysis
In the builders of Recipe & Ingredient entities from the previous article, we see that both classes share some communalities :
The private built entity
builtEntityThe constructor that initializes
builtEntityThe build method that returns the
builtEntityonce the building is done
One way to enhance the code would be to move the common behaviors of the 2 builders in a shared class.
Refactoring
Let's first declare a protected property builtEntity in the class, that will hold the entity being built.
export default class EntityBuilder<BuiltEntityType> {
protected builtEntity: BuiltEntityType
}
The < ... > in the class definition denotes of the usage a type generic. This define a type or class that will use a parameter type, not yet known by the class or the type at the time of its definition.
As such the builtEntity property type will be of the one provided as type parameter of the EntityBuilder class
We can implement the build method, that will return the builtEntity.
public build(): BuiltEntityType {
const returnedObj = this.builtEntity
this.builtEntity = new this.creator()
return returnedObj
}
Note, once again that we use the
BuiltEntityTypeparameter type, this time as the output of thebuildmethod.
To initialize the builtEntity let's implement the constructor of the EntityBuilder class.
constructor(TCreator: new () => BuiltEntityType) {
this.creator = TCreator
this.builtEntity = new this.creator()
}
As the types from typescript are not present in the js runtime, the constructor need to have as input a generator of BuiltEntityType entities, that it can use to generate the new entities. To indicate that this generator should be generating BuiltEntityType we use the following typescript type syntax : new () => BuiltEntityType. This generator passed as input will be, very conveniently, the js class of the entity being built.
We just need to make Ingredient Builder & Recipe Builder derives from the EntityBuilder class thanks to the extends keyword and we can get read of the constructor and the build method in those classes.
This gives us :
Entity builder
export default class EntityBuilder<BuiltEntityType> {
protected builtEntity: BuiltEntityType
private creator: new () => BuiltEntityType
constructor(TCreator: new () => BuiltEntityType) {
this.creator = TCreator
this.builtEntity = new this.creator()
}
public build(): BuiltEntityType {
const returnedObj = this.builtEntity
this.builtEntity = new this.creator()
return returnedObj
}
}
Ingredient builder
import EntityBuilder from 'src/entityBuilder'
import Ingredient, { EKind } from 'src/useCases/business/entities/Ingredient'
export default class IngredientBuilder extends EntityBuilder<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
}
}
Recipe builder
import EntityBuilder from 'src/entityBuilder'
import Ingredient from 'src/useCases/business/entities/Ingredient'
import Recipe, { EDifficulty } from 'src/useCases/business/entities/Recipe'
export default class RecipeBuilder extends EntityBuilder<Recipe> {
public static anIngredient() {
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
}
}
Conclusion
Using typescript generics and the derivation of class we simplified our 2 builders.
It is good, but with this very few things are checked at built time, the typescript compiler ignoring some error or making some erroneous ones :

In this sample, the weightInGrams property is defined, while typescript thinks is not. And name is undefined while typescript think it is. Also we will need to implement a builder for each entities.
Let's see how we can enhance things a bit in the next part.
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




