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 symbolfields or helper generics likeBrand<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
instanceofchecks, 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.