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 the build 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 :

Step 1 - Typescript errors.png

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