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 newunion 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 theunion 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 anobject 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
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