Skip to main content

TS Ep 53: Branding and Flavoring (Nominal Typing)

Rachmat Hidayat
Author
Rachmat Hidayat
Learn & sharing insights on TypeScript, Go, Kubernetes, DevOps, DevSecOps, SRE, Platform Engineering, AI/ML Engineering, and MLOps.
typescript - This article is part of a series.
Part 53: This Article
TypeScript uses Structural Typing (Duck Typing). If it walks like a duck and quacks like a duck, it’s a duck. But what if we desperately need to differentiate between a UserId and a PostId when both are just basic strings at runtime?

1. The Problem: Structural Equivalence
#

In a Nominal Type System (like Java, C#, or Go), the compiler throws an error if you pass a variable of type PostId into a function expecting UserId, because the names of the types are different.

In TypeScript’s Structural System, type names are just aliases. They don’t mean anything.

type UserId = string;
type PostId = string;

function deleteUser(id: UserId) {
  // Database deletion logic
}

const myPostId: PostId = "post_999";

// 🔴 DANGER! 
// The compiler allows this because both are just structurally 'string'!
// You just deleted a user by passing a post ID.
deleteUser(myPostId);

2. The Solution: Branding (Opaque Types)
#

To force TypeScript to treat them differently, we must change their structure. We do this by attaching a fake, “phantom” property to the primitive type. This property does not exist at runtime, but the compiler sees it in Type Space.

This technique is called Branding (or creating Opaque Types).

// We intersect the primitive type K with a unique structural property.
type Brand<K, T> = K & { __brand: T };

// Now they are structurally different!
type UserId = Brand<string, "UserId">;
type PostId = Brand<string, "PostId">;

function deleteUser(id: UserId) {
  // Logic
}

// 🟢 You must explicitly cast the string to a Branded type when it enters your system
const myUserId = "user_1" as UserId;
const myPostId = "post_1" as PostId;

deleteUser(myUserId); // SAFE!

// 🔴 ERROR! Argument of type 'PostId' is not assignable to parameter of type 'UserId'.
// Types of property '__brand' are incompatible.
// deleteUser(myPostId);

Why use an Intersection?
#

By using an intersection (&), the core type remains a standard string. The __brand is just a hallucination for the compiler. When deleteUser receives the UserId, it still possesses all the standard prototype methods of a string:

function deleteUser(id: UserId) {
  // Valid! It's still fundamentally a string!
  const upper = id.toUpperCase();
}

3. Flavoring (Optional Branding)
#

Branding is incredibly strict. If you attempt to pass a heavily branded string into a generic 3rd-party library function that expects a standard string, it will sometimes complain.

If you want a slightly softer approach, you can use Flavoring.

Flavoring makes the phantom property optional (?). This allows you to pass a branded type into a generic function, but still prevents you from passing a different branded type into a highly specific function.

type Flavor<K, T> = K & { __flavor?: T };

type EmailAddress = Flavor<string, "Email">;
type HomeAddress = Flavor<string, "Address">;

function sendEmail(email: EmailAddress) {
  // Logic
}

// You still have to cast when it enters the system boundary
const validEmail = "test@test.com" as EmailAddress;
const home = "123 Main St" as HomeAddress;

// 🔴 Prevented!
// sendEmail(home); 

// 🟢 The benefit: You can pass an EmailAddress to a generic string function without issues
function getLength(str: string) { return str.length; }
getLength(validEmail); // Works perfectly!

Summary & Next Steps
#

In this episode:

  • We discussed the limitations of Structural Typing when dealing with generic primitives like IDs.
  • We implemented Branding by intersecting primitives with phantom __brand properties.
  • We implemented Flavoring by making the brand optional, allowing interoperability with generic libraries.

Branding is the ultimate defense mechanism for Domain-Driven Design (DDD) in TypeScript, ensuring you never mix up UUIDs, currencies, encrypted strings, or sanitized HTML.

In Episode 54: The Builder Pattern with Generics, we will explore how to chain generic methods together to build complex configurations step-by-step!

typescript - This article is part of a series.
Part 53: This Article