Advanced TypeScript
Branded & Opaque Types
Branded types add a phantom marker to structurally identical values so TypeScript treats them as distinct domain concepts, such as UserId and ProductId even when both are strings at runtime.
- Branding adds nominal friction to structural typing
- The brand is compile-time only unless validated
- Use brands for domain identifiers and units
- Do not brand everything
- Keep brand construction centralized
type Brand<T, B extends string> = T & { readonly __brand: B }
type UserId = Brand<string, "UserId">
type ProductId = Brand<string, "ProductId">
function parseUserId(value: string): UserId {
if (!value.startsWith("user_")) throw new Error("Invalid UserId")
return value as UserId
}
function loadUser(id: UserId) {}
const id = parseUserId("user_123")
loadUser(id)| Concept | Runtime shape | Why brand |
|---|---|---|
| UserId vs ProductId | string | Prevent ID mixups |
| Milliseconds vs seconds | number | Prevent unit bugs |
| string | Require validation | |
| Access token | string | Avoid logging/mixing |
| Normalized path | string | Ensure canonicalization |