brand
Nominal typing via a phantom tag — Brand, Opaque, Unbrand.
import type {
Brand, Branded, Unbrand, Tagged, Opaque, Nominal, BrandOf,
} from '@gentleduck/ttest/brand'
Branded types attach a phantom tag to a base type, producing values that are runtime-compatible but type-incompatible with the unbranded base. The tag stores both the base type and the brand name, so Unbrand and BrandOf can recover them. For the full pattern see Guides / Branded types.
Brand
type Brand<T, B extends string | symbol>
Nominal wrapper. Brand<string, 'UserId'> is incompatible with Brand<string, 'OrderId'> and with plain string. Runtime values are unaffected — the tag is phantom.
type UserId = Brand<string, 'UserId'>
type OrderId = Brand<string, 'OrderId'>
const u = 'u_1' as UserId
const o: OrderId = u // Error — UserId is not OrderId
const s: string = u // Error — string isn't a UserId either (the inverse holds via Unbrand)
// Brands compose with object shapes.
type Email = Brand<string, 'Email'>
type User = { id: UserId; email: Email }
Branded
type Branded<T, B extends string | symbol> = Brand<T, B>
Alias of Brand. Use whichever reads better in context — some codebases prefer Branded<T, 'Tag'> for the adjectival form.
Unbrand
type Unbrand<T>
Strip the brand, recovering the base type. Returns T unchanged for unbranded values.
type UserId = Brand<string, 'UserId'>
type a = Unbrand<UserId> // string
type b = Unbrand<string> // string (pass-through)
type c = Unbrand<number> // number
// Serializing a brand at the boundary.
function serialize(id: UserId): Unbrand<UserId> {
return id // base type — no cast needed downstream
}
Tagged
type Tagged<T, Tag extends string, TagValue = unknown>
Brand with a custom __${Tag} discriminant key. Interoperates with third-party brand conventions (type-fest, ts-essentials) that key on a specific property name.
type Money = Tagged<number, 'currency', 'USD'>
// ^? number & { __currency: 'USD' }
const m: Money = 100 as Money
const eur: Tagged<number, 'currency', 'EUR'> = m // Error — different __currency
Opaque
type Opaque<T, B extends string | symbol = string> = Brand<T, B>
Semantic alias of Brand borrowed from the type-fest vocabulary. Pick the name that fits your codebase.
type Token = Opaque<string, 'JWT'>
Nominal
type Nominal<T, B extends string | symbol> = Brand<T, B>
Another semantic alias — favored in OO-influenced codebases that talk in nominal/structural terms.
type EmployeeId = Nominal<number, 'EmployeeId'>
BrandOf
type BrandOf<T>
Recover the brand tag of a branded T, or never for unbranded values.
type UserId = Brand<string, 'UserId'>
type a = BrandOf<UserId> // 'UserId'
type b = BrandOf<string> // never
import type { AssertTrue } from '@gentleduck/ttest/assert'
import type { Equal } from '@gentleduck/ttest/equality'
// Lock the brand tag in a test — defends against accidental brand renames.
type _ = AssertTrue<Equal<BrandOf<UserId>, 'UserId'>, 'brand tag is UserId'>
Edge case: Equal<Brand<string, 'X'>, string> is false — branded types do not equal their base. This is by design but a frequent confusion. See Core / Composing tests for the brand-aware assertion pattern.