Branded Types & Nominal Typing
Adding compile-time safety with branded types
TypeScript has structural typing — any type with the same shape is compatible. Branded types add nominal behavior for extra safety.
What you'll learn
Understand structural vs nominal typing
Implement the brand pattern (T & { __brand: B })
Use flavoring for lightweight nominal typing
Add runtime validation paired with branded types
Prevent mixing up IDs, emails, and other primitives
Apply branding to real-world function signatures
The Problem: Structural Typing
Two different concepts with the same shape are interchangeable.
type UserID = string
type Email = string
type PostID = string
function getUser(id: UserID) {
/* ... */
}
function sendEmail(to: Email) {
/* ... */
}
const userId: UserID = "user_123"
// Oops — no error, but wrong semantic type
sendEmail(userId) // compiles fine
The Brand Pattern
Add a phantom property that exists only at compile time.
type Brand<T, B> = T & { __brand: B }
type UserID = Brand<string, "UserID">
type Email = Brand<string, "Email">
type PostID = Brand<string, "PostID">
function getUser(id: UserID): void {
/* ... */
}
function sendEmail(to: Email): void {
/* ... */
}
const userId = "user_123" as UserID
const email = "alice@example.com" as Email
getUser(userId) // OK
getUser(email) // Type error: Email is not UserID
sendEmail(email) // OK
sendEmail(userId) // Type error: UserID is not Email
Gotcha
The as cast is a type-level assertion. For real safety, create constructor
functions that validate at runtime and return the branded type.
Runtime Validation + Compile-Time Safety
Pair branded types with runtime checks.
type Brand<T, B> = T & { __brand: B }
type Email = Brand<string, "Email">
type UserID = Brand<string, "UserID">
// Constructor with runtime validation
function createEmail(value: string): Email {
if (!value.includes("@")) throw new Error("Invalid email")
return value as Email
}
function createUserID(value: string): UserID {
if (!value.startsWith("user_")) throw new Error("Invalid user ID")
return value as UserID
}
// Safe constructors
function sendEmail(to: Email, body: string): void {
console.log(`Sending to ${to}: ${body}`)
}
const email = createEmail("alice@example.com")
const id = createUserID("user_42")
sendEmail(email, id) // Type error
Flavoring Pattern
A lighter-weight alternative that doesn't affect runtime behavior.
// Flavor — brands are optional, not enforced
type Flavor<T, F> = T & { __flavor?: F }
type USD = Flavor<number, "USD">
type EUR = Flavor<number, "EUR">
function formatMoney(amount: USD): string {
return `$${amount.toFixed(2)}`
}
const price = 29.99 as USD
const rate = 0.92 as EUR
formatMoney(price) // OK
formatMoney(rate) // Type error (flavored)
Real-World: Domain Primitives
type Brand<T, B> = T & { __brand: B }
// Domain types
type Phone = Brand<string, "Phone">
type SSN = Brand<string, "SSN">
type OrderID = Brand<string, "OrderID">
type Quantity = Brand<number, "Quantity">
function validatePhone(input: string): Phone {
if (!/^\+?[\d\s-]{7,15}$/.test(input)) throw new Error("Invalid phone")
return input as Phone
}
function validateSSN(input: string): SSN {
if (!/^\d{3}-\d{2}-\d{4}$/.test(input)) throw new Error("Invalid SSN")
return input as SSN
}
// Function signatures encode domain rules
function createOrder(
userId: UserID,
items: Array<{ id: OrderID; qty: Quantity }>,
contact: Phone
): void {
/* ... */
}
Effect on Function Signatures
Branding forces callers to be explicit about their types.
type Brand<T, B> = T & { __brand: B }
type APIKey = Brand<string, "APIKey">
type BearerToken = Brand<string, "BearerToken">
function authenticate(token: BearerToken): { userId: string } {
// decode JWT…
return { userId: "user_1" }
}
function fetchResource(apiKey: APIKey, resource: string): Promise<unknown> {
// use API key…
return Promise.resolve({})
}
// Cannot accidentally mix them up
const key = "sk-abc123" as APIKey
const token = "eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoiMSJ9.xxx" as BearerToken
authenticate(key) // Type error
fetchResource(token, "users") // Type error
Branded User Entities
Code
typescript
1type Brand<T, B> = T & { __brand: B }2type UserId = Brand<string, "UserId">3type Email = Brand<string, "Email">4 5interface SafeUser {6id: UserId7email: Email8}9 10type SafeResult =11| { ok: true; user: SafeUser }12| { ok: false; error: string }13 14// Implement safeUser that validates raw input15function safeUser(input: unknown): SafeResult {16 17}Key Takeaways
Structural typing treats same-shape types as identical — branding adds nominal distinctions.
The Brand pattern (T & { __brand: B }) adds a phantom type property.
Flavoring uses optional flavors for lighter-weight nominal distinctions.
Pair branded types with runtime validation constructors for defense in depth.
Domain primitives (Email, UserID, Phone) prevent mixing up semantically different values.
Branded function signatures force callers to be explicit about which domain type they're passing.