Skip to main content

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.