Brand — Distinguishing Values
27 B minified + gzipped
A transfer desk accepts a warehouse identifier and a shipment identifier, both strings. A reversed argument compiles cleanly, the shipment is filed against the wrong warehouse, and the audit report still shows a successful transfer.
Core Behavior
Section titled “Core Behavior”Brand<K, T> intersects T with a phantom label K, so two values with the same representation
become distinct TypeScript types.
Brand.wrap<K, T>()returns a function that applies the label; the runtime value is unchanged and unchecked.- A branded value is assignable to its underlying type
T, butTis not assignable back to the brand. Brand.unwrapreturns the underlyingT; the brand exists only in the type system.- The label carries no runtime marker, so data read from JSON or a database needs parsing before it is branded.
Reach for Brand when two values share a representation but must never be exchanged. When the same
boundary must also reject malformed input, Result carries the parsed value or a
typed error before the brand is applied. The choosing guide covers the
containers that carry validation outcomes; a brand records the outcome once validation has passed.
Example: Warehouse Identifiers
Section titled “Example: Warehouse Identifiers”A warehouse transfer needs a warehouse ID and a shipment ID. Both arrive as strings, but a function that expects one should not accidentally receive the other.
The brand changes the type accepted by TypeScript without changing the value passed at runtime:
flowchart TD Raw["Raw identifier string"] --> Wrap["Brand.wrap"] Wrap --> Typed["Branded identifier for the type checker"] Typed --> Runtime["Same string at runtime"]
Validating and Using Identifiers
Section titled “Validating and Using Identifiers”The parsing functions check each prefix before applying its brand. The transfer function then accepts the two distinct types:
The transfer function accepts a WarehouseId and a ShipmentId. Swapping the arguments is a type
error even though both values are strings.
Parsing at the boundary
Section titled “Parsing at the boundary”The parsers above open-code the prefix check. At an HTTP or queue boundary, the same check composes
as a Result pipeline that rejects the raw string before any brand reaches the
domain:
Core Operations
Section titled “Core Operations”Creating Brands
Section titled “Creating Brands”| Signature | What it does | Result |
|---|---|---|
Brand<K, T> |
Adds a type label to T. |
A branded type assignable to T. |
Brand.wrap<K, T>() |
Creates an unchecked branding function. | (value: T) => Brand<K, T>. |
Reading Branded Values
Section titled “Reading Branded Values”| Signature | What it does | Result |
|---|---|---|
Brand.unwrap(branded) |
Returns the value with its underlying type. | T. |
A branded value also passes directly where T is expected; unwrap states that intent in code.
Failure Behavior and Edge Cases
Section titled “Failure Behavior and Edge Cases”Brand.wrap accepts any value of the underlying type, including an invalid identifier. The brand
records a type distinction; it does not enforce a format. Validate at the input boundary, as in the
example, when the format matters to the application.
The distinction is one-directional. A WarehouseId satisfies a parameter typed string, so a
function that does not ask for the brand still accepts it. Protection holds only where signatures
name the branded type.
The label is erased at runtime. A value that crosses JSON, a database, or a message queue arrives as
its plain representation and must be parsed and branded again on the way back in. A type assertion
restores the label without checking the value; use Result at that boundary when
the format is not guaranteed.
Brand.unwrap is an unchecked conversion in the opposite direction. It performs no validation and
returns the original value.