Skip to main content

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.