Photo by Olga Filonenko on Unsplash
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
builtEntity
The constructor that initializes
builtEntity
The build method that returns the
builtEntity
once 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
BuiltEntityType
parameter type, this time as the output of thebuild
method.
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