createBrandedId(config)

Creates a BrandedId class — a typed wrapper around a string ID that prevents accidentally mixing up different ID types. UserId and ProductId are incompatible at compile time, even though both wrap strings.

import { z } from 'zod';
import { createBrandedId } from '@sotajs/ddd';

const UserId = createBrandedId({
  brand: 'UserId',
  schema: z.string().uuid(),
});
type UserId = ReturnType<typeof UserId.create>;

const ProductId = createBrandedId({
  brand: 'ProductId',
  schema: z.string().uuid(),
});
type ProductId = ReturnType<typeof ProductId.create>;

// Compile-time safety:
function assignToUser(id: UserId) {}
assignToUser(productIdInstance); // TypeScript error

Config

FieldRequiredDescription
brandYesUnique string literal. Used for TypeScript branding.
schemaNo*Zod/Valibot schema for the string value.
typeNo*ArkType type.
validateNo*Generic (value: string) => string. Can sanitize.

*One of schema, type, or validate is required.


Instance API

MemberTypeDescription
BrandedId.create(value)BrandedId instanceValidates and returns instance.
id.valuestring & { __brand }The branded string.
id.equals(other)booleanValue comparison.
id.toString()stringReturns the raw string.
id.toJSON()stringJSON serialization — returns the string.

Why this exists

TypeScript is structurally typed. Without branded IDs:

type UserId = string;
type ProductId = string;

function getUser(id: UserId) {}
function getProduct(id: ProductId) {}

const productId = 'abc-123';
getUser(productId); // compiles — wrong ID passed

With branded IDs, the compiler catches the mistake. At runtime, validation still applies — schema or validate function runs on every .create().

toJSON() and toString() return the plain string — serialization is transparent. You can store branded IDs in JSON, pass them to APIs, use them as route params. Deserialize through .create() to get validation back.


Validation + sanitization

The validator can also sanitize:

const TrimmedId = createBrandedId({
  brand: 'Trimmed',
  validate: (v) => v.trim().toLowerCase(),
});

const id = TrimmedId.create('  HELLO  ');
id.value; // 'hello'

Pitfalls

  • Brand is compile-time only. At runtime, the value is a plain string.
  • Schema must validate a string. z.string().uuid() works. z.object({...}) does not.
  • TypeScript-only safety. JavaScript consumers can still mix up IDs, but runtime validation applies.