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
| Field | Required | Description |
|---|---|---|
brand | Yes | Unique string literal. Used for TypeScript branding. |
schema | No* | Zod/Valibot schema for the string value. |
type | No* | ArkType type. |
validate | No* | Generic (value: string) => string. Can sanitize. |
*One of
schema,type, orvalidateis required.
Instance API
| Member | Type | Description |
|---|---|---|
BrandedId.create(value) | BrandedId instance | Validates and returns instance. |
id.value | string & { __brand } | The branded string. |
id.equals(other) | boolean | Value comparison. |
id.toString() | string | Returns the raw string. |
id.toJSON() | string | JSON 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()andtoString()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.