Advanced TypeScript

Branded & Opaque Types

Branded types add a phantom marker to structurally identical values so TypeScript treats them as distinct domain concepts, such as UserId and ProductId even when both are strings at runtime.
  • Branding adds nominal friction to structural typing
  • The brand is compile-time only unless validated
  • Use brands for domain identifiers and units
  • Do not brand everything
  • Keep brand construction centralized
Branded IDs
type Brand<T, B extends string> = T & { readonly __brand: B }

type UserId = Brand<string, "UserId">
type ProductId = Brand<string, "ProductId">

function parseUserId(value: string): UserId {
  if (!value.startsWith("user_")) throw new Error("Invalid UserId")
  return value as UserId
}

function loadUser(id: UserId) {}

const id = parseUserId("user_123")
loadUser(id)
The parser is the trusted place where runtime value becomes branded type.
Good branding candidates
ConceptRuntime shapeWhy brand
UserId vs ProductIdstringPrevent ID mixups
Milliseconds vs secondsnumberPrevent unit bugs
EmailstringRequire validation
Access tokenstringAvoid logging/mixing
Normalized pathstringEnsure canonicalization
Sources
  • Effective TypeScript, 2nd EditionStructural typing and branding
  • The TypeScript HandbookType compatibility
  • TypeScript ReferenceIntersection types