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
@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
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.
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.
Iso to help us jump from opaque to private, and from private to opaque. It replace the
toOpaque functions from the previous article.
The second kind of optics
MessageUnion exposes is
Lens allows you to look “inside” a type, either to get or to change something.
Lens to access properties of the private types hidden in opaque types.
This enables us to easily expose queries and transformation functions.
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!