Branded types in Typescript
🏠 Go homeSometimes, 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!