Emulation of nominal typing in TypeScript

Come to think of it, this title sounds a bit like “The Knights of the Year 1000 at Lake Paladru”… Do you get the reference? Let me walk you through it, starting with a few definitions.
Nominal or structural typing
We refer to nominal typing when a type is defined by its name. Java uses nominal typing. If two types have different names—for example, Numerator and Denominator—the compiler will treat them as different even if these types are actually identical (same behavior).
This contrasts with structural typing, in which a type is defined by its structure. TypeScript supports structural typing. For example, still on the topic of fractions, the types Numerator and Denominator defined below are fully interchangeable from the compiler’s perspective:
type Numerator = {
value: number;
};
type Denominator = {
value: number;
};
This is called duck typing, because if it flies and quacks like a duck, then it’s a duck 🦆
Limitations of structural typing
The creators of TypeScript’s choice of structural typing offers many advantages, which are widely documented on the web. Unfortunately, this choice proves limiting for those who want to practice defensive programming, which aims to “ensure the continued operation of software under unforeseen circumstances.”
This approach assumes a (justifiably) paranoid mindset, which forbids us from trusting anyone—whether a user or even a developer: whether intentional or not, sooner or later you’ll receive data that will cause the program to fail. Example: the infamous division by 0.
The idea, then, is to perform the necessary validations as early as possible and to encapsulate the validated data within an abstraction that can be safely manipulated throughout the rest of the program. Hence the recommendation to encapsulate the language’s primitives (string, number, etc.) and to bring out the corresponding business abstractions (Domain-Driven Design, my love 💜).
Thus, we cannot simply rely on the previously defined types Numerator and Denominator. Since these types are identical from the compiler’s perspective, they can be mistakenly swapped by a developer or user. Sooner or later, a denominator will be mistaken for a numerator (problematic), or vice versa (even more problematic, as this could lead to division by 0).
In other words, we want the guarantees of nominal typing, while working with structural typing.
What should be done?
Emulation of nominal typing
Well, we can still emulate nominal typing. To do this, we’ll use:
-
A symbol (
DENOMINATOR_SYMBOL), which we will be careful not to export; -
A
createDenominator—a function that accesses the symbol and uses it in the object it creates. This performs the checks without which it would be incorrect to refer to it as a denominator.
Thus:
-
The types
NumeratorandDenominatorare no longer interchangeable; -
Creating a
Denominatorobject is only possible through the constructor, thereby preventing null denominators.
Isn’t life beautiful? 🏝️🍸
The complete code is available on GitHub: https://github.com/mathieueveillard/nominal-typing-emulation
Branding / flavoring
I demonstrated this approach using the example of type Numerator = { value: number; };, but in “real life,” when there is only a single value to encapsulate—which is particularly the case with identifiers—you’ll likely want to use a lighter version, known as branding or flavoring.
type Branded<T> = string & { __brand: T };
which we’ll use as follows:
const CLIENT_SYMBOL = Symbol();
type ClientId = Branded<typeof CLIENT_SYMBOL>;
The idea is the same, except that we’re working with a language primitive (in this case, string). This is easier to work with than an object, since the type created this way is a subtype of string.