Branded Types: Safer IDs, Money, and Domain Values
Use branded (nominal-ish) types to prevent mixing IDs and domain values, and learn safer alternatives to ‘just cast it’.
On this page
What you’ll learn
- Why TypeScript is structurally typed (and what that means for domain safety)
- How branded types prevent mixing
UserIdwithOrderId - Safer ways to create brands than
as UserId
30‑second interview answer
TypeScript is structurally typed, so two string values are interchangeable even if they represent different concepts. Branded types add a phantom property to create nominal-ish distinctions like UserId vs OrderId. This prevents entire classes of bugs in large codebases, especially around IDs, currency, and validated values.
The problem: structural typing mixes meanings
type UserId = string;
type OrderId = string;
function loadUser(id: UserId) {}
const orderId: OrderId = "ord_123";
loadUser(orderId); // ✅ compiles (bad!)Brand pattern
type Brand<T, B extends string> = T & { readonly __brand: B };
export type UserId = Brand<string, "UserId">;
export type OrderId = Brand<string, "OrderId">;Now this fails:
function loadUser(id: UserId) {}
const orderId = "ord_123" as OrderId;
loadUser(orderId); // ❌ Type errorThe dangerous part: as UserId
You can still lie:
const userId = "not-a-real-id" as UserId;So the important engineering question is: how do we create branded values safely?
Safer construction: parse/validate at boundaries
function isUserId(s: string): s is UserId {
return /^usr_[a-z0-9]+$/.test(s);
}
export function parseUserId(s: string): UserId {
if (!isUserId(s)) throw new Error("Invalid UserId");
return s;
}
const id = parseUserId("usr_ab12"); // UserIdThis is what you do in production: validate at I/O boundaries.
Example: Money type (avoid mixing currencies)
type Currency = "INR" | "USD";
type Money<C extends Currency> = Brand<
{ amount: number; currency: C },
`Money:${C}`
>;
function money<C extends Currency>(amount: number, currency: C): Money<C> {
return { amount, currency } as Money<C>;
}
function add<C extends Currency>(a: Money<C>, b: Money<C>): Money<C> {
return money(a.amount + b.amount, a.currency);
}
const a = money(100, "USD");
const b = money(200, "USD");
add(a, b);
// add(a, money(50, "INR")) // ❌ type errorProduction rule of thumb
- Use brands for values that have domain meaning (IDs, emails, URLs, money).
- Don’t brand everything; brand the “high-cost mistakes”.
- Create branded values through parsers/validators at boundaries, not random casts.
Interview questions
-
Q: Why do branded types work in TS?
- A: The phantom property makes the type structurally different while having zero runtime cost.
-
Q: Where should validation happen?
- A: At I/O boundaries (API responses, forms, DB reads), so internal code stays safe.
Quick recap
- TS is structural; brands add nominal-ish safety.
- Brands prevent mixing different
stringmeanings. - Validate once at boundaries, then pass branded values internally.
Looking for structured, offline interview prep?
Free online lessons • Portable Amazon Kindle / Paperback
Frontend Interview Guide (2025 Edition)
By Rajesh Dhiman
Comprehensive preparation covering React internals, Web Performance, CSS architecture, and system design questions.
JavaScript Interview Guide
By Rajesh Dhiman
Deep dive into the Event Loop, microtask draining, closures, prototypal inheritance, and asynchronous execution patterns.
Note: All lessons on Frontend Interview remain completely free to read online. As an Amazon Associate, we may earn from qualifying purchases.
Written by Rajesh Dhiman
Authored by Rajesh Dhiman and engineered at Eunix Tech. Focused on deep-dive frontend engineering, JavaScript runtime internals, React architecture, and web performance.