Brand — Distinguishing Values
TypeScript’s type system is built on structural typing. If two types have the same shape, they are treated as compatible and fully interchangeable.
Usually, this structural compatibility is a major strength — it allows us to compose objects and interfaces with minimal ceremony. However, when working with primitive values like identifiers, measurement units, or validated strings, structural typing can work against us.
In standard TypeScript, every string is structurally compatible with every other string.
Consider this everyday scenario:
The compiler sees a string, receives a string, and remains silent. Yet, passing a customer
identifier to a function expecting a user identifier is a clear semantic bug. The type system has
failed to capture our design intent because string is too permissive.
Brand<K, T> solves this. It adds a compile-time phantom tag K to a primitive type T. It
allows us to overlay a nominal (named) type system on top of TypeScript’s structural one:
The underlying values remain plain JavaScript strings, but the compiler now treats UserId and
CustomerId as completely distinct, incompatible types.
Creating and Wrapping Brands
Section titled “Creating and Wrapping Brands”To lift a raw primitive value into a branded context, we first declare a wrapping constructor using
Brand.wrap:
At the compile level, passing a CustomerId to a function expecting a UserId will now trigger a
static type error:
This error is resolved entirely at compile time.
Unwrapping Values
Section titled “Unwrapping Values”Because Brand<K, T> structurally extends the underlying type T, any branded value is naturally
assignable back to its raw type without requiring any conversion:
If you prefer to make this unwrapping explicit in your code to document your boundary transitions,
you can use Brand.unwrap:
Zero Runtime Cost
Section titled “Zero Runtime Cost”The brand tag exists solely for the benefit of the TypeScript compiler. The compiled JavaScript output contains no wrapper objects, no class instantiations, and no tag fields on the actual values.
Brand.wrap and Brand.unwrap compile directly down to identity functions: x => x. They incur
zero runtime memory allocation and zero CPU overhead.
Structural Integrity: Smart Constructors
Section titled “Structural Integrity: Smart Constructors”Branding becomes exceptionally powerful when combined with validation to build Smart Constructors.
A standard brand constructor like toUserId is unchecked — it trusts you to supply a valid string.
For branded types that must enforce invariants (such as a valid email address, a non-empty string,
or a positive integer), we wrap the brand creator in a validation function:
By hiding the raw toEmail constructor and only exporting parseEmail, we guarantee that it is
structurally impossible to instantiate an Email type that has not passed validation.
Downstream functions that accept the Email type can trust it completely, bypassing redundant
validation checks:
When to use Brand
Section titled “When to use Brand”Use Brand when:
Section titled “Use Brand when:”- Preventing identifier mixing: You want to distinguish between
UserId,OrderId, andProductIdto prevent query mismatches. - Enforcing validation invariants: You want to lock down validated domains like
Email,Slug, orSecureHtmlusing smart constructors. - Distinguishing metrics: You want to prevent arithmetic errors by separating units like
Seconds,Meters, orKilograms.
Keep using raw types when:
Section titled “Keep using raw types when:”- Structural compatibility is desired: You are modeling objects where structural compatibility is the intended architectural behavior.
- Working with complex structures: You are wrapping rich object models that already carry sufficient type distinction through their interface structures.