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’.
F
Frontend Interview Team
March 01, 2026
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.