Using Branded Types to Prevent Unit Confusion in TypeScript
1 min read
The problem
function getUser(id: string): User { /* ... */ }
function getOrder(id: string): Order { /* ... */ }
// Compiles fine. Runtime disaster.
const user = getUser(orderId);
The solution: branded types
type Brand<T, B> = T & { __brand: B };
type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;
function getUser(id: UserId): User { /* ... */ }
function getOrder(id: OrderId): Order { /* ... */ }
// Now this won't compile ✗
getUser(orderId);
// This will ✓
getUser(userId);
Going further with nominal typing
// Type-safe IDs with validation
function createUserId(raw: string): UserId | null {
if (!/^usr_[a-z0-9]{16}$/.test(raw)) return null;
return raw as UserId;
}
// Now invalid IDs are caught at the boundary
const id = createUserId(req.params.id);
if (!id) return res.status(400).json({ error: "invalid user id" });
const user = getUser(id);
What I learned
Branded types add zero runtime cost but catch real bugs. The pattern shines in codebases with multiple ID types — payment systems, e-commerce, any domain where numbers and strings carry different semantics.