Branded Types
A branded type adds a phantom "brand" to a base type — like {'string & '} — so TypeScript treats two otherwise-identical types as incompatible, simulating nominal typing on top of its structural system.
Learn Branded Types in our free TypeScript course — an interactive lesson with runnable examples, a practice exercise and a quick reference.
Part of the free TypeScript course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
Two banknotes can look identical, but a watermark proves which is genuine and which currency it is. A brand is that watermark for your types: a user id and an order id are both "just strings," but the invisible brand stamps each with its true purpose. The watermark isn't part of the paper's value — and likewise the brand vanishes at runtime — yet it stops you spending dollars as if they were euros.
1. Adding a Phantom Brand
TypeScript is structural, so a UserId that's "just a string" is interchangeable with any other string — including an OrderId . Branding breaks that on purpose: intersect the base type with a phantom property ( {'& '} ) that nothing ever sets at runtime. Now UserId and OrderId have different types, so the compiler refuses to mix them.
2. Why Brand: Ids and Units
The motivation is preventing values that share a type but not a meaning from mixing. User ids vs order ids is one case; physical units are another famous one — meters and feet are both numbers, and silently adding them has caused real, costly failures. Brand them and the compiler forbids the mix, requiring an explicit conversion function to move between them.
3. Smart Constructors: Validation as Proof
Branding gets even more powerful with a smart constructor : a single function that validates raw input and only then brands it. Because that constructor is the only way to produce the branded type, any value of that type is a proof that validation already passed. Downstream code can trust an Email contains an @ without re-checking — eliminating repetitive guards and "I forgot to validate" bugs.
🎯 Your Turn
A Positive brand guarantees a number is greater than zero. Fill in the blank marked ___ to halve it, then run it.
No blanks this time — just a brief and a starting outline. Build the constructor and lookup yourself, run it, and check your output against the example in the comments.
Practice quiz
What does a branded type let you simulate on top of TypeScript's structural system?
- Dynamic typing
- Runtime reflection
- Nominal typing - compatibility by name, not just shape
- Duck typing
Answer: Nominal typing - compatibility by name, not just shape. Branding makes two structurally-identical types distinct, simulating nominal typing (compatibility by name) atop structural typing.
How is a branded type like UserId typically written?
- string & { readonly __brand: "UserId" }
- string | { __brand: "UserId" }
- type UserId = new string()
- Brand<UserId>
Answer: string & { readonly __brand: "UserId" }. You intersect the base type with a phantom brand property: string & { readonly __brand: "UserId" }.
Does the brand property exist at runtime?
- Yes, as a real property on the value
- Only in strict mode
- Only on objects
- No - it is type-only and erased during compilation
Answer: No - it is type-only and erased during compilation. The brand is a phantom that lives only in the type system; at runtime a branded UserId is just an ordinary string.
Why is branding described as essentially free?
- It needs no npm package
- It has zero runtime cost and adds no extra fields
- It compiles faster than normal types
- It removes the need for tests
Answer: It has zero runtime cost and adds no extra fields. Brands are erased at compile time, so there's zero runtime cost and no extra property - all protection is at compile time.
If UserId and OrderId are both branded strings, what happens when you pass an OrderId where a UserId is required?
- It is a compile-time error because the brands differ
- It works because both are strings
- It silently converts at runtime
- It throws at runtime
Answer: It is a compile-time error because the brands differ. Different brands make the types incompatible, so passing an OrderId for a UserId is a compile error - exactly the point.
Can a plain, unbranded string be assigned to a branded UserId?
- Yes, always
- Only if it is non-empty
- No - it must pass through a constructor first
- Only inside an async function
Answer: No - it must pass through a constructor first. A raw string isn't assignable to a branded type; that's deliberate, forcing every value through a smart constructor.
What is a smart constructor in the context of branded types?
- A class constructor with default parameters
- The single function that validates raw input, then brands it
- A generic factory for any type
- A constructor that runs at compile time
Answer: The single function that validates raw input, then brands it. A smart constructor validates then brands - and because it's the only way to mint the type, the value proves validation passed.
Why can downstream code trust a value typed Email without re-validating?
- Email types are always non-empty
- TypeScript revalidates automatically
- Email values are immutable
- The only way to get an Email is through the validating constructor
Answer: The only way to get an Email is through the validating constructor. Since the branded Email can only come from makeEmail (which validates), any Email is proof validation already passed.
Why does TypeScript's structural typing allow ids and units to be mixed by default?
- Because it checks types by name
- Because two values with the same shape (e.g. both strings) are considered the same type
- Because it ignores all type errors
- Because numbers and strings are interchangeable
Answer: Because two values with the same shape (e.g. both strings) are considered the same type. Structural typing compares shape, so a UserId that's 'just a string' is interchangeable with any other string until you brand it.
Which reusable helper makes defining brands a one-liner?
- type Brand = any
- interface Brand extends string {}
- type Brand<T, B> = T & { readonly __brand: B }
- type Brand<T> = Partial<T>
Answer: type Brand<T, B> = T & { readonly __brand: B }. A generic helper type Brand<T, B> = T & { readonly __brand: B } lets every brand be defined in a single line.