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.,
stringornumber) with a phantom property keyed by a unique brand (string literal orunique 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 symbolas 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.,
toUpperCaseon 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.