Skip to content

Transform.brand Compile-time

Validation modes: Validation modes reference

Declaration. Attaches a compile-time nominal brand to a schema's TypeScript type. Returns the same schema object at runtime, no WeakMap entry is created, no runtime effect. The TypeScript return type becomes BrandedType<TSchema, TBrand>, which intersects TSchema with a unique-symbol-keyed tag { readonly [BRAND]: TBrand } (not a string field). The string-keyed shape { readonly brand: TBrand } is on BrandOutputType (the inferred value-level type), not on BrandedType (the schema-level type). Access the branded value type via BrandOutputType<typeof schema>.

Use this when you need nominally distinct types for identifiers that are structurally identical at runtime - CustomerId and OrderId are both UUID strings, but TypeScript should refuse to let you pass one where the other is expected. This prevents mixing up ID fields from different entity types.

Don't use this when you need an automatic decode/encode transformation (use Transform.create). Don't use it for automatic runtime validation beyond what JSON Schema already provides - brand is purely a compile-time marker.

Examples

Example 1: Nominally distinct Customer and Order IDs

/**
 * Transform.brand — Example 1: Nominally distinct Customer and Order IDs
 * Demonstrates: BrandOutputType, compile-time incompatibility, instantiate to brand
 *
 * CustomerIdSchema and OrderIdSchema are both UUID strings at runtime, but
 * their branded TypeScript types are mutually incompatible. The canonical
 * Bastian Balthazar Bux fixtures provide the concrete UUID values.
 */

import { Transform } from '../../../src/index.js';
import type { BrandOutputType } from '../../../src/types/index.js';
import {
  aboxFixtures,
  createBookstoreDocRegistry
} from '../bookstore/index.js';

// createBookstoreDocRegistry seeds a permissive copy of the bookstore — docs examples extend
// it with ad-hoc demo schemas; strict-graph checking is intentionally off here.
const jt = createBookstoreDocRegistry();

const BrandedCustIdSchema = Transform.brand(
  {
    '$id': 'https://bookstore.example/BrandedCustId',
    'format': 'uuid',
    'type': 'string'
  } as const,
  'CustomerId'
);

const BrandedOrdIdSchema = Transform.brand(
  {
    '$id': 'https://bookstore.example/BrandedOrdId',
    'format': 'uuid',
    'type': 'string'
  } as const,
  'OrderId'
);

type CustomerId = BrandOutputType<typeof BrandedCustIdSchema>;

jt.set(BrandedCustIdSchema);
jt.set(BrandedOrdIdSchema);

// The only way to obtain a branded value — go through instantiate.
const cid = jt.instantiate(BrandedCustIdSchema, aboxFixtures.customer.customerId);

// At compile time: CustomerId ≠ OrderId — the types are nominally distinct.
function lookupCustomer(_: CustomerId): void {
  // no-op in this demonstration
}
// OK — typed correctly
lookupCustomer(cid as CustomerId);

console.assert(typeof cid === 'string');
console.assert(cid === aboxFixtures.customer.customerId);
console.log('CustomerId (branded):', cid);
// Both are plain strings at runtime; the brand is compile-time only.
console.log('runtime typeof cid  :', typeof cid);
Output
Press Execute to run this example against the real library.

Example 2: Branded ISBN for books

/**
 * Transform.brand — Example 2: Branded ISBN for books
 * Demonstrates: pattern-validated brand, compile-time nominal type for ISBN13
 *
 * An ISBN-13 brand prevents passing a plain unvalidated string where a
 * validated ISBN is expected. The canonical 1979 Thienemann Verlag first
 * edition of Michael Ende's Die unendliche Geschichte provides the fixture ISBN.
 */

import { Transform } from '../../../src/index.js';
import type { BrandOutputType } from '../../../src/types/index.js';
import {
  aboxFixtures,
  createBookstoreDocRegistry
} from '../bookstore/index.js';

// createBookstoreDocRegistry seeds a permissive copy of the bookstore — docs examples extend
// it with ad-hoc demo schemas; strict-graph checking is intentionally off here.
const jt = createBookstoreDocRegistry();

const BrandedIsbn13Schema = Transform.brand(
  {
    '$id': 'https://bookstore.example/BrandedIsbn13',
    'pattern': '^\\d{13}$',
    'type': 'string'
  } as const,
  'ISBN13'
);

type ISBN13 = BrandOutputType<typeof BrandedIsbn13Schema>;

jt.set(BrandedIsbn13Schema);

// lookupBook(isbn: ISBN13) prevents passing plain unvalidated strings.
function lookupBook(isbn: ISBN13): string {
  return isbn;
}

const isbn = jt.instantiate(
  BrandedIsbn13Schema,
  aboxFixtures.rareBook.isbn
);

const result = lookupBook(isbn as ISBN13);

console.assert(result === aboxFixtures.rareBook.isbn);
console.assert(typeof isbn === 'string');
// ISBN13 branded value: 9783522128001
console.log('ISBN13 (branded):', isbn);
// A plain string literal cannot be passed to lookupBook without going through instantiate
console.log('accepted by lookupBook:', result);
Output
Press Execute to run this example against the real library.

Bad examples - what NOT to do

Anti-pattern 1: Applying brand after the schema has been registered

/**
 * Transform.brand — Anti-pattern contrast: brand before registration
 * Demonstrates: brand must be applied before set(); the registered schema
 * carries the branded type, not a post-hoc application
 *
 * The correct pattern is shown here: brand first, then register.
 * The canonical Walter Moers author name provides the string fixture.
 */

import { Transform } from '../../../src/index.js';
import {
  createBookstoreDocRegistry
} from '../bookstore/index.js';

// createBookstoreDocRegistry seeds a permissive copy of the bookstore — docs examples extend
// it with ad-hoc demo schemas; strict-graph checking is intentionally off here.
const jt = createBookstoreDocRegistry();

// ✓ Correct: brand before registration — the registered schema carries the
// branded TypeScript type.
const BrandedAuthorId = Transform.brand(
  {
    '$id': 'https://bookstore.example/BrandedAuthorId',
    'type': 'string'
  } as const,
  'AuthorId'
);

jt.set(BrandedAuthorId);

// The branded schema is retrievable from the registry.
console.assert(jt.registry.has(BrandedAuthorId.$id));
console.log('registered schema ID:', BrandedAuthorId.$id);

// Instantiate produces a value typed as AuthorId at compile time.
const authorId = jt.instantiate(BrandedAuthorId, 'walter-moers');

console.assert(typeof authorId === 'string');
console.assert(authorId === 'walter-moers');
// AuthorId branded value: walter-moers
console.log('AuthorId branded value:', authorId);
Output
Press Execute to run this example against the real library.

Comparison

ts
const CustomerIdSchema = Transform.brand(
  { $id: 'https://bookstore.example/CustomerId', type: 'string', format: 'uuid' } as const,
  'CustomerId',
);
type CustomerId = BrandOutputType<typeof CustomerIdSchema>;
// string & { readonly brand: 'CustomerId' }
ts
const CustomerIdSchema = z.string().uuid().brand<'CustomerId'>();
type CustomerId = z.infer<typeof CustomerIdSchema>;
// string & z.BRAND<'CustomerId'>
ts
import * as v from 'valibot';
const CustomerIdSchema = v.pipe(v.string(), v.uuid(), v.brand('CustomerId'));
type CustomerId = v.InferOutput<typeof CustomerIdSchema>;
// string with brand 'CustomerId'
ts
import * as t from 'io-ts';
interface CustomerIdBrand { readonly CustomerId: unique symbol }
const CustomerIdCodec = t.brand(
  t.string,
  (input): input is t.Branded<string, CustomerIdBrand> =>
    /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(input),
  'CustomerId',
);
type CustomerId = t.TypeOf<typeof CustomerIdCodec>;
// string & Brand<{ readonly CustomerId: unique symbol }>
ts
// TypeBox does not have a built-in brand utility.
// Use TypeScript's type-level branding manually:
type CustomerId = string & { readonly __brand: 'CustomerId' };
// No schema-level enforcement  - brand is a TypeScript-only type alias.
ts
// Not applicable  - AJV provides no TypeScript type branding.
py
from typing import Annotated, NewType

# NewType creates nominal types in Python:
CustomerId = NewType('CustomerId', str)

# Or use Annotated with a validator:
from pydantic import AfterValidator
CustomerIdType = Annotated[str, AfterValidator(lambda v: v)]
ts
// Limitation: feature not directly supported in Yup. See /comparisons for the matrix.
ts
// Limitation: feature not directly supported in Joi. See /comparisons for the matrix.
ts
// Limitation: feature not directly supported in Effect Schema. See /comparisons for the matrix.
ts
// Limitation: feature not directly supported in ArkType. See /comparisons for the matrix.
ts
// Limitation: feature not directly supported in Runtypes. See /comparisons for the matrix.

See also

Released under the MIT License.