Skip to content
webtype.orgBrand a primitive

    ↑↓ move · ⏎ open · esc close

    Patterns

    How do I stop a `UserId` being passed where an `OrderId` was expected, when both are just strings?

    TypeScript is structural, so two aliases of `string` are the same type. A phantom property makes them different without changing the value.

    The recipe

    declare const brand: unique symbol
    
    type Brand<T, Name extends string> = T & { readonly [brand]: Name }
    
    type UserId = Brand<string, 'UserId'>
    type OrderId = Brand<string, 'OrderId'>
    
    const asUserId = (raw: string): UserId => raw as UserId
    
    declare function findUser(id: UserId): void
    
    const id = asUserId('u_1')
    findUser(id)

    The build compiles this and checks each result below.

    How it works

    1. 01
      declare const brand: unique symbol

      A `unique symbol` gives a key nothing else can produce. A string key would let an unrelated object accidentally satisfy the brand.

    2. 02
      type Brand<T, Name extends string> = T & { readonly [brand]: Name }

      The intersection adds a property that exists only in the type. At run time the value is still exactly the string you started with.

    3. 03
      const asUserId = (raw: string): UserId => raw as UserId

      One cast, in one place, is the price. Everywhere else the compiler enforces the distinction for free — which is why this function should be the only way in.

    What you get

    Where it goes wrong

    The brand is erased, so `JSON.parse` will happily hand you something typed `UserId` that never went through your constructor. Brands guard your code against itself; they do not validate data crossing a boundary.

    Takeaway

    Reach for a brand when two values share a representation and must not share a meaning. Keep exactly one function that produces each one.

    See also

    Patterns