Opaque Types in TypeScript

Example of Opaque Types

Today we discuss Opaque types:

  1. What problems do they solve
  2. What ways could we solve this problem
  3. Why I chose this solution
  4. Describe the solution in more technical details

The problem

TypeScript, like Elm and Haskell, has a structural type system. It means that 2 different types but of the same shape are compatible:

Example of structural type system

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.

Probable solutions

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:

Example of a class with private property

Code in Playground

2. Class + intersection types

We still define class here, but for every nominal type we have Generic type:

Example of a class with intersection types

Example in Playground

3. Type + intersection types

We only define type here and use Generic type with intersection types:

Example of a type with intersection types

Have a look at Playground

4. Type + intersection types + unique symbol

We still define type, use Generic type, use intersection types with unique symbol:

Example of a type with intersection types and unique symbol

The example in Playground

Choose the solution

Let’s compare all the approaches that are mentioned above:

ApproachError readabilityJS-freeCan be reusedEncapsulated
Class + a private property5️⃣❌ class + constructor
Class + intersection types5️⃣❌ empty class
Type + intersection types5️⃣__brand visibility in TS
Type + intersection types + unique symbol5️⃣
  1. All approaches have a great error readability (the problem is visible and it’s connected to the nominal type)
  2. 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)
  3. By encapsulation here Type + intersection types make __brand property 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

unique symbol

It’s possible to create a symbol in TypeScript without creating it in JavaScript. So it won’t exist after compiling

Declare unique symbol

Also, if you plan to reuse OpaqueType and put it to the separate file:

Example of an Opaque type implementation

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.

Example

Let’s have a look at CodeSandbox

ts-opaque-units example

It uses ts-opaque-units which implements Opaque function with unique symbol. For instance, Days is defined as:

Days example

Resources

  1. Nominal typing techniques in TypeScript
  2. Implementing an opaque type in typescript
  3. Support some non-structural (nominal) type matching #202
typescript

Let's chat and have fun 🧑‍💻👩‍💻

No more often than once a week I write 📄 about TypeScript 💪, video playback 📺 and frontend related topics. You can unsubscribe in any time ↘️