Mathieu Eveillard

Emulating nominal typing in TypeScript

Structural typing is limiting when it comes to defensive programming. Fortunately, it is possible to emulate nominal typing (branding).

Emulating 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

Nominal typing means that 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

TypeScript’s creators chose structural typing, which offers many advantages, 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.

So what can we do?

Emulating 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 constructor (createDenominator), a function that accesses the symbol and uses it in the object it creates. This constructor performs the checks without which it would be incorrect to refer to it as a denominator.

Thus:

  • The types Numerator and Denominator are no longer interchangeable;

  • Creating a Denominator object is only possible through the constructor, thereby preventing zero denominators.

Isn’t life grand? 🏝️🍸

The complete code is available on GitHub: https://github.com/mathieueveillard/nominal-typing-emulation

Branding / flavoring

I demonstrated this approach using objects as the example (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 };

We’ll use it 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.

← All posts