Dynamic methods with Typescript

Photo by Jan Huber on Unsplash

Dynamic methods with Typescript

Typescript builder tutorial [4/6]

Introduction

To get rid of the build error we had in the previous article, we will need to make our builder methods "dynamic". To achieve that, we will use one of the powerful feature of typescript, the definition of types.

Toying with types

We will define the following type :

type BuilderWithTypeMethods<T> = EntityBuilder<T> & {
  [K in keyof T]:
  (value: T[K]) => BuilderWithTypeMethods<T>
}

Let's take the time to analyse this type. We see that our type is using :

  • The op1 & op2 operator allows to merge the two types provided as the two operands.
    type mergedTypes = { x: string } & { y: number } // { x: string, y: number }
    
  • The keyof op1 operator gets a new union type constituting of all the key of the type provided as the right operand.
    type possibleKeys = { x: string, y: number } // 'x' | 'y'
    
  • The op1 in op2 operator gets an iterator from all the possible types of a the union type provided to its right operand an assign it to its left operand. It can only be used in the definition of the key of an object type
    type keys = 'x' | 'y'
    type objectWithStringProperties = {
      [K in keys] : string
    } // { x: string, y: string }
    

Knowing all this we can deduce that the BuilderWithTypeMethods<T> type is an EntityBuilderType for T on which for each of the properties of type T a method named from the property name which have on parameter named value which type is the type of the property on the type T , methods that returns an object of type BuilderWithTypeMethods<T>. The definition of this method is done with (value: T[K]) => BuilderWithTypeMethods<T> .

We can also make use of the standard typescript type Partial<T> , that from an object type builds another one with all of its properties optional, to enforce the fact that the resulting object of the builder might not have all its properties set.

type Partial<T> = {
    [P in keyof T]?: T[P]
}

Using our newly defined types

We can then use this BuilderWithTypeMethods<T> and the Partial<T> to enhance our builder class so that the builder class exposes methods that match the properties of the type of the object building built & enforcing the fact that the built object might not be complete.

type BuilderWithTypeMethods<T> = EntityBuilder<T> & {
  [K in keyof T]:
  (value: T[K]) => BuilderWithTypeMethods<T>
}
export default class EntityBuilder<BuiltEntityType> {

  public static getA<StaticBuiltEntityType>(TCreator: new () => StaticBuiltEntityType) {

    return new EntityBuilder(TCreator) as unknown as BuilderWithTypeMethods<StaticBuiltEntityType>
  }

  public static getAn<StaticBuiltEntityType>(TCreator: new () => StaticBuiltEntityType) {
    return EntityBuilder.getA(TCreator)
  }

  protected builtEntity: Partial<BuiltEntityType>
  private creator: new () => BuiltEntityType

  constructor(TCreator: new () => BuiltEntityType) {
    this.creator = TCreator
    this.builtEntity = new this.creator()
  }

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

}

Conclusion

With this updated code our sample code show that it is more usable

Step 3 - Errors on valuated properties.png

It is better, but there is still ways for improvements, first one would to have better method name (withKind instead of kind) and second one would be to have the correct undefined status on fields.

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