Today we discuss Opaque types:
- What problems do they solve
- What ways could we solve this problem
- Why I chose this solution
- Describe the solution in more technical details
TypeScript, like Elm and Haskell, has a structural type system. It means that 2 different types but of the same shape are compatible:
It leads to more flexibility but at the same time leaves a room for specific bugs.
Nominal typing system, on the other hand, would throw an error in this case because types don't inherit each other so no instance of one type cannot be assigned to the instance of another type.
TypeScript didn't resolve nominal type feature and since 23 Jul 2014 has an open issue: Support some non-structural (nominal) type matching #202.
Ryan Cavanaugh described the cases in the comment where nominal types would be useful.
Let's see how we can imitate nominal type feature for TypeScript 4.2:
1. Class + a private property
Here we define
class for every nominal type and add
__nominal mark as a private property:
2. Class + intersection types
We still define
class here, but for every nominal type we have Generic type:
3. Type + intersection types
We only define
type here and use Generic type with intersection types:
4. Type + intersection types + unique symbol
We still define
type, use Generic type, use intersection types with
Choose the solution
Let's compare all the approaches that are mentioned above:
|Approach||Error readability||JS-free||Can be reused||Encapsulated|
|Class + a private property||5️⃣||❌ class + constructor||❌||✅|
|Class + intersection types||5️⃣||❌ empty class||✅||✅|
|Type + intersection types||5️⃣||✅||✅||❌ |
|Type + intersection types + unique symbol||5️⃣||✅||✅||✅|
- All approaches have a great error readability (the problem is visible and it's connected to the nominal type)
- First 2 approaches use JS: Class + a private property cannot be reused, Class + intersection types can be reused but still creates empty class (which is fine)
- By encapsulation here Type + intersection types make
__brandproperty visible outside and can lead to stupid errors which I want to get rid of.
So if you don't really want to see one empty class, please use Type + intersection types + unique symbol
If one empty class is still okay, you can choose Class + intersection types
I will stop on Type + intersection types + unique symbol
Also, if you plan to reuse
OpaqueType and put it to the separate file:
It's a good idea as in this case
symbol won't be accessible outside of the file and therefore you cannot read the property.
Let's have a look at CodeSandbox
It uses ts-opaque-units which implements
Opaque function with unique symbol. For instance,
Days is defined as: