Patterns
How do I stop a UserId being passed where an OrderId was expected, when both are just strings?
TypeScript is structural, so two aliases of string are the same type. A phantom property makes them different without changing the value.
The recipe
declare const brand: unique symbol type Brand<T, Name extends string> = T & { readonly [brand]: Name } type UserId = Brand<string, 'UserId'> type OrderId = Brand<string, 'OrderId'> const asUserId = (raw: string): UserId => raw as UserId declare function findUser(id: UserId): void const id = asUserId('u_1') findUser(id)
The build compiles this and checks each result below.
How it works
- 01
declare const brand: unique symbolA
unique symbolgives a key nothing else can produce. A string key would let an unrelated object accidentally satisfy the brand. - 02
type Brand<T, Name extends string> = T & { readonly [brand]: Name }The intersection adds a property that exists only in the type. At run time the value is still exactly the string you started with.
- 03
const asUserId = (raw: string): UserId => raw as UserIdOne cast, in one place, is the price. Everywhere else the compiler enforces the distinction for free — which is why this function should be the only way in.
What you get
UserId extends string ? true : false→truestring extends UserId ? true : false→falseThe asymmetry is the whole feature: a branded id is still a string, but a bare string is not a branded id.
UserId extends OrderId ? true : false→false
Where it goes wrong
The brand is erased, so JSON.parse will happily hand you something typed UserId that never went through your constructor. Brands guard your code against itself; they do not validate data crossing a boundary.
Takeaway
Reach for a brand when two values share a representation and must not share a meaning. Keep exactly one function that produces each one.

