Branded types in Typescript

🏠 Go home
Typescript Typing 2022-08-12

Sometimes, we'd like to introduce a new type based on an existing one in a way that we don't actually add a new information to it. For example, if we deal with currencies, we might introduce new types

type EUR = number;
type USD = number;

and have our functions nicely annotated like this.

const usdToEur = (eur: EUR): USD => undefined;

The problem is we can do something as follows.

declare const getMoneyInEur: () => EUR;

const moneyUSD: USD = getMoneyInEur();

The type-checker is happy about that because USD and EUR are effectively same types. But, we'd want the type-checker to find as much problems for us as possible. What we can do is to tag the number type so that EUR and USD are actually different from each other.

type EUR = number & { _tag: 'EUR' };
type USD = number & { _tag: 'USD' };

Now, the moneyUSD assignment won't compile, thus we made the code immune to such errors.

const moneyUSD: USD = getMoneyInEur();

Now, there is an interesting problem which is sometimes a feature. If we had a type DifferentUSD defined the same way we defined the USD, the following code would type-check.

type DifferentUSD = number & { _tag: 'USD' };

declare const getMoneyInUsd: () => USD;

const moneyUSD2: DifferentUSD = getMoneyInUsd();

This is due to Typescript's support of structural subtyping. It is actually pretty handy most of the time, but if we need to enforce our function takes or returns only our specific tagged type, we need to do better. And it turns out we can, using Symbols.

Javascript Symbol

Symbols are pretty interesting feature of Javascript. They are objects created using Symbol constructor and they have an important property that every created symbol is equal only to itself. Let's see an example.

const symbol1 = Symbol();
const symbol2 = Symbol('foo');
const symbol3 = Symbol('foo');

symbol1 == symbol1 // true
symbol1 == symbol2 // false
symbol2 == symbol3 // false
Symbol('foo') === Symbol('foo')  // false

Branded types using unique symbol

Funnily enough, we don't need Symbols per se. We only need their types and the property of uniqueness, but now in the type-level code.

Typescript 2.7 added support for unique symbol type. The documentation states:

Each reference to a unique symbol implies a completely unique identity that’s tied to a given declaration.

Therefore if we create following types (the _tag must be readonly)

type USD = number & { readonly _tag: unique symbol };
type DifferentUSD = number & { readonly _tag: unique symbol };

and try to do the same mismatch of USD and DifferentUSD, we'll get an error this time.

declare const getMoneyInUsd: () => USD;

const moneyUSD: DifferentUSD = getMoneyInUsd(); // <- doesn't type-check

That's because USD and DifferentUSD are now not nominally nor structurally subtypes because the type of _tag in each of them has its own identity tied to the type declaration!

Happy coding!