Brand — Distinguishing Values
TypeScript uses structural typing: any string is compatible with every other string. This allows
semantic bugs—such as passing an OrderId into a function expecting a UserId—to compile without
warnings.
Brand<K, T> attaches a compile-time phantom tag K to an underlying type T, enforcing nominal
type safety at zero runtime cost:
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:
Problems it solves
Section titled “Problems it solves”- Preventing accidental identifier swapping: In database queries and service calls, functions
often accept multiple string IDs (such as
senderId,recipientId, andorganizationId). Because standard TypeScript uses structural typing, swapping these arguments goes unnoticed by the compiler.Brandcreates distinct nominal types (likeUserIdandAccountId) that catch parameter mismatches at compile time with zero runtime overhead. - Enforcing validation boundaries with smart constructors: Functions that accept emails, slugs,
or formatted telephone numbers often repeat regex checks defensively or assume incoming strings
are valid.
Brandpairs nominal types with smart constructors, ensuring that once a string passes validation at the API edge, downstream business logic can rely on that invariant without re-validating. - Security-critical input sanitization: Distinguishing sanitized, safe HTML (
SanitizedHtml) or safe SQL fragments from raw user-submitted text prevents XSS and injection vulnerabilities by enforcing at compile time that raw strings cannot be passed directly into dangerous rendering or query APIs. - Eliminating unit and currency mixups: In calculations involving units (such as
MillisecondsvsSeconds, orUsdCentsvsEurCents), raw numbers allow arithmetic across incompatible units.Brandattaches compile-time units to primitives, preventing mathematical bugs across domain layers.