Branded Types in TypeScript
Branded Types in TypeScript
Branded types provide deeper specificity and uniqueness on top of primitive types, enabling compile-time checks that prevent mixing incompatible values.1
The Problem
When you model entities with primitive types, TypeScript treats all values of the same primitive as interchangeable. For example:
type User = { id: string; name: string }
type Post = { id: string; ownerId: string; comments: Comment[] }
type Comment = { id: string; authorId: string; body: string }
async function getCommentsForPost(postId: string, authorId: string) { /* ... */ }
const comments = await getCommentsForPost(user.id, post.id)
// No compile-time error—even though arguments are swapped
Because User["id"]
, Post["id"]
, and Comment["id"]
are all just string
, TSC can’t catch mismatches at compile time.
Naive Branded Type Implementation
A common community pattern is to “tag” a primitive with a brand:
type Brand<K, T> = K & { __brand: T }
type UserID = Brand<string, "UserId">
type PostID = Brand<string, "PostId">
type CommentID = Brand<string, "CommentId">
async function getCommentsForPost(postId: PostID, authorId: UserID) { /* ... */ }
const comments = await getCommentsForPost(user.id, post.id)
// ❌ TypeScript now errors on swapped IDs
Downsides of this naive approach
- The
__brand
property is visible in Intellisense and can be misused (it isn’t present at runtime). - Nothing prevents duplicate brands (multiple
Brand<string, "X">
could collide). - The tag is purely compile-time and may confuse developers.
Improved Branded Type Utility
A stronger implementation hides the brand key and enforces uniqueness:
// Brand.ts
declare const __brand: unique symbol
type Brand<B> = { [__brand]: B }
export type Branded<T, B> = T & Brand<B>
- Uses a
unique symbol
to prevent collisions. - Keeps the property key inaccessible at runtime and in Intellisense.
Comparison of Implementations
Implementation | Definition | Downsides |
---|---|---|
Naive | typescript<br>type Brand<K, T> = K & { __brand: T }<br> | Intellisense noise; duplicate brands; runtime invisibility |
Improved | typescript<br>declare const __brand: unique symbol<br><br>type Brand<B> = { [__brand]: B }<br>export type Branded<T, B> = T & Brand<B><br> | Hides brand key; enforces uniqueness; no collisions |
Why Use Branded Types?
- Clarity: Express intent—e.g.,
Username
vs. rawstring
. - Safety & Correctness: Catch mismatches (ID swaps, unit errors) at compile time.
- Maintainability: Communicate data roles more clearly and simplify refactoring.
Use Cases
1. Custom Validation
type EmailAddress = Branded<string, "EmailAddress">
function validateEmail(email: string): EmailAddress {
// validation logic…
return email as EmailAddress
}
2. Domain Modeling
type CarBrand = Branded<string, "CarBrand">
type EngineType = Branded<string, "EngineType">
function createCar(brand: CarBrand, engine: EngineType) { /* … */ }
3. API Responses & Requests
type ApiSuccess<T> = T & { __apiSuccess: true }
type ApiFailure = { code: number; message: string; error: Error } & { __apiFailure: true }
type ApiResponse<T> = ApiSuccess<T> | ApiFailure
Now you can write precise type guards and avoid mishandling responses.
Challenge
Create a branded Age
type bounded to [0, 125]
:
- Define
type Age = Branded<number, "Age">
. - Implement
createAge(input: number): Age
that throws if out of range. - Implement
getBirthYear(age: Age): number
.
Visualization
Conclusion
Branded types are a lightweight way to introduce nominal typing into TypeScript. They enhance type safety, improve code clarity, and reduce runtime errors by making incompatible values unassignable at compile time. Use them judiciously to model domain concepts, validate inputs, and handle API contracts with confidence.
Footnotes
https://egghead.io/blog/using-branded-types-in-typescript "Improve Runtime Type Safety with Branded Types in TypeScript"˄
Backlinks