Code Snippets
/

Branded (Nominal) Types

Branded (Nominal) Types

TypeScript is structurally typed, so `type UserId = string` and `type OrderId = string` are interchangeable to the compiler, which is exactly the bug class you want to prevent. Branded (nominal) types attach a phantom tag so the two stay distinct at compile time without any runtime cost. This snippet covers the basic brand pattern, a parser-style smart constructor that produces a brand from a raw value, and a multi-brand domain model that ties them together.

TypeScript
Medium
3 snippets
ts-utility-types
ts-type-narrowing
ts-generics
code-template

705 views

20

type Brand<T, B> = T & { readonly __brand: B };

type UserId = Brand<string, 'UserId'>;
type OrderId = Brand<string, 'OrderId'>;

function asUserId(s: string): UserId {
    return s as UserId;
}

function loadUser(id: UserId) {
    return `loaded ${id}`;
}

const u = asUserId('u_123');
console.log(loadUser(u));

The trick is the intersection with { readonly __brand: B }: the structural part stays compatible with the underlying primitive, but the phantom property makes two brands incompatible because their __brand literal types differ. The __brand field never exists at runtime; it is purely a compile-time marker. Wrapping construction in asUserId is the convention so the cast happens in exactly one place. After that point, loadUser cannot accept a raw string or an OrderId, which catches the entire family of mixed-up-id bugs.