Dynamic optional and mandatory fields with advanced Typescript generics
Typescript builder tutorial [5/6]

Introduction
In order to have the correct undefined status for fields on the built objects, we need to be able to have the type of builder that evolves as methods are called upon him. The idea here will be to have the return type of any of the methods of the builder to be a builder type of a new-entity that is the initial entity with the additional field added to it.
Advanced generics at the rescue...
To do that we will need a parametric type that from one initial type extends it with a mandatory property. This could be done through this type:
type TypeWithAdditionalMandatoryProps<T, P extends keyof T> = T & {
[K in NonNullable<keyof Pick<T, P>>]: NonNullable<Pick<T, P>[K]>
}
Some explanations about this type :
<T, P extends keyof T>: Tels the type has two parameters. Thanks to thatP extends keyof T, it tells that the second parameters should be the name of one or more of the properties of the first parameter.
type NT<T, P extends keyof T> = {}
type OK = NT<{ x:string, y: number }, 'x'> // OK
type Erroneous = NT<{ x:string, y: number }, 'z'> // Type '"z"' does not satisfy the constraint '"x" | "y"'.
- The
T & ...tells that it will extend the initial first type with another type Pick<T, P>: From an object typeTcreates a new one, only constituted of the properties provided inP:
type PickedXY = Pick<{ x: string, y: number, z: boolean }, 'x' | 'y'> // { x: string, y:number }
NonNullable: Removesnullandundefinedtypes from a list of union types.
type NonNullableTypes = NonNullable< string | null | undefined | boolean > // string | boolean
As a whole we have a type TypeWithAdditionalMandatoryProps that from an input type T and a list of properties P of T build a new type T-Bis with all the properties P set as mandatory properties.
... with type recursion to spice things a bit
We can then enhance the BuilderWithTypeMethods as follow :
type BuilderWithTypeMethods<T> = EntityBuilder<T> & {
[K in NonNullable<keyof T>]: (value: NonNullable<T[K]>) => BuilderWithTypeMethods<TypeWithAdditionalMandatoryProps<T, K>>
}
Here also some explanation might be needed
- The type
BuilderWithTypeMethodscallsBuilderWithTypeMethodsmaking it a recursive type. The recursion of type was not supported in the first version of typescript, and this now allow some advance type construction. - The addition of the
NonNullablekey word allow to get rid of potentiallyundefinedmethods when building optional properties from the input type.
The type now says that each methods of the builder returns a builder with a type being built is the same type but with an additional property.
Here a sample of usage :
const cheddarBuilderWithKInd = EntityBuilder.getAn(Ingredient)
.kind(EKind.Cheese) // BuilderWithTypeMethods<TypeWithAdditionalMandatoryProps<Partial<Ingredient>, "kind">>
const cheddarBuilderWithKIndAndWeight = cheddarBuilderWithKInd
.weightInGrams(170) // BuilderWithTypeMethods<TypeWithAdditionalMandatoryProps<TypeWithAdditionalMandatoryProps<Partial<Ingredient>, "kind">, "weightInGrams">>
const cheddar = cheddarBuilderWithKIndAndWeight
.build() // TypeWithAdditionalMandatoryProps<TypeWithAdditionalMandatoryProps<Partial<Ingredient>, "kind">, "weightInGrams">
Dynamic property names
Let's add a last modification to rename the name of the method so that it is more builder like :
type BuilderWithTypeMethods<T> = EntityBuilder<T> & {
[K in NonNullable<keyof T> as `with${Capitalize<string & K>}`] :
(value: NonNullable<T[K]>) => BuilderWithTypeMethods<TypeWithAdditionalMandatoryProps<T, K>>
}
- The keyword
asallow to rename theKproperty - The
askeyword can use template string to construct complexe transformation of theKname. In our case we use the built-inCapitalizetype to construct a new string type with name being capitalized.
type Capitals = Capitalize<'abc' | 'def'> // 'Abc' | 'Def'
Conclusion
Thanks to the usage of typescript generics, few typescript built-in types and keys words like extends or NonNullable, we were able to build a type that evolves as we use it.
We can then use the our generic builder as follow :
It's magic ! 🎉
So now we have a class, that a type level works as expected (build time), but our class need more polish to be used at running. Thanks to javascript proxies, we will be able to dynamically change the behavior of our builder object. This is what we will do in our next and last post of this typescript series.
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




