Why Branded Types Matter

When a codebase grows, it becomes easy to pass a userId where an orderId is expected. Both are just strings at runtime, so the compiler cannot catch the mistake. Branded types give you nominal typing in a structural language, letting the type checker enforce the distinction without any extra JavaScript.

The Problem: Accidental ID Mix‑ups

I once spent an afternoon debugging a payment failure caused by a function that expected an OrderId but received a UserId. The logs showed a valid UUID, yet the downstream service rejected it because the identifier belonged to a different domain. A simple compile‑time guard would have saved hours.

Implementing a Brand Helper

// brand.ts
type Brand = K & { __brand: T };
type UserId = Brand;
type OrderId = Brand;

function brand(value: K): Brand {
  return value as Brand;
}

// Usage
const userId = brand('usr_123') as UserId;
const orderId = brand('ord_456') as OrderId;

// This will error: userId = orderId; // Type '"OrderId"' is not assignable to type '"UserId"'

Real‑World Scenario: API Layer

In a typical REST client, each endpoint returns a payload typed with its own identifier. By branding the IDs, the compiler guarantees that a function handling user profiles never receives an order identifier, and vice‑versa. The change is isolated to the type definitions; no runtime checks are added.

Guarding Runtime Values

Branded types are erased at compile time, so you still need a small runtime validator when data crosses the trust boundary (e.g., parsing a query string). The validator can return the branded type, keeping the rest of the codebase safe.

function parseUserId(input: string): UserId {
  if (!input.startsWith('usr_')) throw new Error('Invalid user id');
  return brand(input);
}

Why Not Just Use Separate Interfaces?

Interfaces are structural; interface UserId { value: string } and interface OrderId { value: string } are still compatible because they share the same shape. Branded types add a phantom property that exists only in the type system, giving you true nominal discrimination.

Branded types give you nominal safety without runtime overhead.

Putting It All Together

Adopt the brand helper in a shared types package. Export UserId, OrderId, and any other domain identifiers. Every service that consumes the package automatically inherits the compile‑time guard. The result is a codebase where mismatched IDs become impossible to compile, letting you focus on business logic instead of defensive checks.