TypeScript: Branded Types

Perceived Benefits and Use Cases

  • Branded types emulate nominal typing in TypeScript’s structural system.
  • Common use cases:
    • Distinguishing IDs of different entities (UserId vs OrgId vs AccountId).
    • Units and dimensions (nanoseconds vs seconds, inches vs meters, currencies).
    • DDD-style value objects (Email, String100, SafeString, LoggedUser, verified cookies).
    • Separating “raw” vs “validated/parsed” data at system boundaries; helpful with hexagonal architectures.
  • Helps prevent argument-order bugs when several parameters share the same primitive type.
  • In medium/large codebases and type-heavy libraries (validation, DB access, FP utilities), advanced typing allows more expressive, safer APIs.

Concerns About Complexity and Safety

  • Some find this pattern overengineered, ugly, or not worth it for typical apps that use simple types.
  • Fear that advanced type tricks encourage unreadable “type spaghetti” and slow compilation.
  • Branding can be bypassed with casts (as), so it is not a security feature and can hide misuse.
  • Library consumers may get confusing error messages when branded types are used heavily.

Implementation Techniques Discussed

  • Core trick: intersect a base type with a unique “brand” property, often:
    • type X = T & { readonly __X_brand: never }
    • or using unique symbol fields or helper generics like Brand<T, "Tag">.
  • Typically requires a constructor function or explicit cast to apply the brand.
  • Works especially well for primitives; for objects, meaningful field names or classes may already suffice.
  • Classes with private fields give nominal behavior and allow instanceof checks, but introduce runtime overhead.

Type System Philosophy and Alternatives

  • Debate over structural vs nominal typing; some see branding as fighting the language design, others as a pragmatic zero-cost escape hatch.
  • Desire for first-class nominal/refinement/newtype support in TypeScript; related work and internal branded types exist.
  • Comparisons made to Flow’s opaque types, Go’s distinct types, Haskell newtypes, Rust wrappers, and Odin’s distinct.

Runtime Validation vs Static Guarantees

  • Branding is often paired with parsing/validation functions so that once a value is branded, callers can rely on invariants statically.
  • Some argue runtime checks and tests are sufficient; others prefer shifting as many invariants as possible into the type system.