How to model your entities with opaque and sum types in Typescript — round 2
In a previous article we shared how we use opaque and sum types to model a new domain in Typescript.
We built a simple way to write a robust and maintainable domain (in the sense of Domain Driven Design), providing a clean and readable basis to build from further down the line. But, as we will see, it came with boilerplate.
Modeling the domain with our basic opaque-types library
This still works great. Our domain is safe: details of the types are kept private and the users of this message.ts
module are forced to use the exposed APIs in order to deal with messages. Opaque types hide implementation hence we cannot use them directly.
Although it produces a very solid domain, there is also a lot of boilerplate to write and it doesn’t accept entity addition as easily as we would like. Imagine adding a new $Audio
type here and having to deal with a new AudioOpaqueAPI
everywhere. Not fun.
Modeling the domain with @iadvize-oss/opaque-union
Enters @iadvize-oss/opaque-union
. This is an experimental library to help with writing opaque sum-types without the boilerplate.
With this library we can rewrite our domain like so:
The exposed domain API is the same but the code is much more direct.. We just had to pick what we wanted to expose out of our MessageUnion
.
Let’s look in more detail at what @iadvize-oss/opaque-union
enables us to do.
First, it enables us to list the private types we will work with and build an “opaque union api”.
We will build our entire APi from this MessageUnion
. Because it has great power, it should not be shared outside the module: it would be counterproductive to expose opaque types but also share the key to “unopaque” them.
From MessageUnion
comes our constructors :
These are functions that build opaque types from private types. We can extract the opaques types (the returned types) from them and we can use them directly like so:
MessageUnion
also gives us type guards and fold for free.
With all these we have now built a convenient message.ts
module that can be used in our app like this:
Constructors, type guards and fold help us a lot but the majority of the boilerplate of the previous example was in query and transformation functions, where we have to “unopaque” a variable to access or modify a property of the private type.
To cut down on this tedium, MessageUnion
exposes some optics from monocle-ts
. You don’t have to be familiar with optics, lenses, prism or profunctor dark magic to use them.
MessageUnion
exposes two kinds of optics. The first one is Iso
. It helps us jump from a type A
to a type B
and back again (when there is a bidirectional link between them — an isomorphism, hence the name). Give them a type A
, it will give you a B
in return. Give them a type B
, it will give you an A
. You get the idea.
MessageUnion
uses Iso
to help us jump from opaque to private, and from private to opaque. It replace the fromOpaque
and toOpaque
functions from the previous article.
The second kind of optics MessageUnion
exposes is Lens
. A Lens
allows you to look “inside” a type, either to get or to change something. MessageUnion
uses Lens
to access properties of the private types hidden in opaque types.
This enables us to easily expose queries and transformation functions.
The function text
here will read the corresponding property inside the opaque type Text it receives. updateText
will update the property, given a new content.
Optics are hard to understand. There is a lot more to say on them (what are the other optics, how they compose well, etc.) but that’s not the goal of the library. For our need — getter, setter, complex transformations on opaque types — Iso and Lens are enough.
With this experimental library we hope to drastically reduce the time needed to write our domain files. We also hope its strong typing and helpful optic helpers will reduce the need to write tedious tests.
This library is already available, feel free to use and experiment with it. Start for example by reading the documentation of the repo. Then a simple npm add @iadvize-oss/opaque-union
will do!
A big thanks ❤️ to all my iAdvize colleagues that helped me write this post: Axel Cateland, Ben, Nicolas !