Branded types for TypeScript

Structural vs nominal typing in TypeScript

  • Many comments frame branded types as a way to simulate nominal typing inside TypeScript’s fundamentally structural type system.
  • Structural typing: types are compatible if their shapes match (e.g., two classes with the same fields can be assigned to each other).
  • Nominal typing: types are distinct by name even if structure matches (e.g., different wrapper types for the same primitive).
  • Some argue TS already has limited nominal features (private members, unique symbol), but not for primitives or across arbitrary aliases.

What branded types do and how they work

  • Core idea: intersect a base type (e.g., string or number) with a phantom property keyed by a unique brand (string literal or unique symbol).
  • The brand is erased at runtime; values remain plain primitives or objects.
  • This enables distinct types for conceptually different values (hashes, different IDs, units, currencies) sharing the same representation.

Alternatives and related features

  • Wrapper classes/structs are proposed as the straightforward nominal solution, but are rejected by some as adding runtime overhead and verbosity.
  • Template literal types can distinguish some string forms (like prefixed IDs) but don’t generalize to arbitrary transforms or non-string data.
  • Flow’s opaque types, Haskell/Idris newtype, Rust/Scala opaque or newtype-like patterns are cited as more “first-class” versions of this idea.
  • TS can fake nominal classes via private fields, or use unique symbol as brands.

Arguments in favor

  • Turn certain runtime bugs (swapped parameters, wrong ID type) into compile-time errors.
  • Zero runtime cost and no wrapper allocations.
  • Improve self-documentation of domain models (e.g., different token types, units, role-specific IDs).
  • Can compose multiple brands on one value and use type operations (Exclude, intersections) for nuanced constraints.

Critiques and limitations

  • Some find the pattern hacky, inelegant, or overkill for typical web apps, preferring simple aliases plus code review discipline.
  • Branding still allows misuse through base-type methods (e.g., toUpperCase on a hash); it only guards where branded types are expected.
  • Heavy use can create friction, confusing error messages, and type-assertion (as) pitfalls.
  • Several posters argue the real fix is native opaque/nominal types in TypeScript rather than clever type tricks.