TypeScript is structural, so `UserId` and `OrderId` (both `string`) are interchangeable. How do you make them incompatible — and what's the cost?
Simulating nominal typing to stop semantically-different values of the same primitive from mixing.
Because TS is structural, two aliases of `string` are freely interchangeable, so nothing stops you passing an OrderId where a UserId is expected — a real class of bugs (mixing ids, currencies, validated vs unvalidated strings). Branding fakes nominal typing by intersecting the primitive with a phantom, unique property that exists only in the type world: `type UserId = string & { readonly __brand: 'UserId' }`. No value actually carries `__brand` at runtime — it's a compile-time tag that makes UserId and OrderId structurally distinct, so they no longer assign to each other or to a plain `string` parameter that expects the brand. You mint a branded value through a single checked constructor (`function toUserId(s: string): UserId { /* validate */ return s as UserId }`) — the one sanctioned `as`, which centralizes validation. Using a `unique symbol` for the brand key makes collisions impossible across modules. The costs: it's a convention, not enforced at runtime (a raw string cast through `as` still slips in — so guard the boundaries), and it adds a little ceremony (constructors, occasional assertions). The payoff is large for values where mixing is dangerous: entity ids, `Email`/`Url` after validation, `Cents` vs `Dollars`, `SafeHtml` vs `string`. It encodes 'this string has been checked / means X' into the type, turning a whole category of mixups into compile errors.
Entity IDs (UserId vs OrderId), validated values (Email, Url, SafeHtml), units (Cents vs Dollars) — anywhere same-typed values must not mix.