Basic Usage
The .brand() modifier creates branded types, which add a compile-time distinction between values that are structurally identical but semantically different.
import { z } from 'zod';
const UserId = z.string().brand<'UserId'>();
const ProductId = z.string().brand<'ProductId'>();
type UserId = z.infer<typeof UserId>; // string & Brand<'UserId'>
type ProductId = z.infer<typeof ProductId>; // string & Brand<'ProductId'>
function getUser(id: UserId) {
// ...
}
const userId = UserId.parse('user-123');
const productId = ProductId.parse('prod-456');
getUser(userId); // OK
getUser(productId); // Type error: ProductId is not assignable to UserId
getUser('user-123'); // Type error: string is not assignable to UserId
Brands are purely a compile-time feature. At runtime, branded values are identical to their base types.
Type Inference
By default, brands only affect the output type:
const UserId = z.string().brand<'UserId'>();
type Input = z.input<typeof UserId>; // string
type Output = z.output<typeof UserId>; // string & Brand<'UserId'>
This means you can parse any string, but the result is branded.
Brand Direction
You can control whether brands apply to input, output, or both:
Output Only (Default)
const UserId = z.string().brand<'UserId'>();
// or explicitly:
const UserId = z.string().brand<'UserId', 'out'>();
type Input = z.input<typeof UserId>; // string
type Output = z.output<typeof UserId>; // string & Brand<'UserId'>
const UserId = z.string().brand<'UserId', 'in'>();
type Input = z.input<typeof UserId>; // string & Brand<'UserId'>
type Output = z.output<typeof UserId>; // string
const UserId = z.string().brand<'UserId', 'inout'>();
type Input = z.input<typeof UserId>; // string & Brand<'UserId'>
type Output = z.output<typeof UserId>; // string & Brand<'UserId'>
Multiple Brands
You can apply multiple brands to create a hierarchy:
const BaseSchema = z.object({ name: z.string() }).brand<'Base'>();
const ExtendedSchema = BaseSchema.brand<'Extended'>();
type Base = z.infer<typeof BaseSchema>;
// { name: string } & Brand<'Base'>
type Extended = z.infer<typeof ExtendedSchema>;
// { name: string } & Brand<'Base'> & Brand<'Extended'>
// Extended is assignable to Base, but not vice versa
const processBase = (x: Base) => x;
const processExtended = (x: Extended) => x;
const base = BaseSchema.parse({ name: 'test' });
const extended = ExtendedSchema.parse({ name: 'test' });
processBase(base); // OK
processBase(extended); // OK - Extended is a subtype of Base
processExtended(extended); // OK
processExtended(base); // Type error
Numeric and Symbol Brands
Brands can be strings, numbers, or symbols:
// Number brand
const Answer = z.number().brand<42>();
type Answer = z.infer<typeof Answer>;
// number & Brand<42>
// Symbol brand
const MyBrand = Symbol('myBrand');
const MyType = z.string().brand<typeof MyBrand>();
type MyType = z.infer<typeof MyType>;
// string & Brand<typeof MyBrand>
// Multiple brands with different types
const Multi = z.number()
.brand<'primary'>()
.brand<typeof MyBrand>();
type Multi = z.infer<typeof Multi>;
// number & Brand<'primary'> & Brand<typeof MyBrand>
Branded Record Keys
const SpecialKey = z.string().brand<'SpecialKey'>();
const schema = z.record(
SpecialKey,
z.number()
);
type Schema = z.infer<typeof schema>;
// Record<string & Brand<'SpecialKey'>, number>
Use Cases
Preventing ID Mixups
const UserId = z.string().brand<'UserId'>();
const OrderId = z.string().brand<'OrderId'>();
const ProductId = z.string().brand<'ProductId'>();
class UserService {
getUser(id: z.infer<typeof UserId>) { /* ... */ }
}
class OrderService {
getOrder(id: z.infer<typeof OrderId>) { /* ... */ }
}
const userId = UserId.parse('user-123');
const orderId = OrderId.parse('order-456');
const userService = new UserService();
userService.getUser(userId); // OK
userService.getUser(orderId); // Type error - prevents mixing up IDs
Safe Numeric Types
const PositiveInt = z.number()
.int()
.positive()
.brand<'PositiveInt'>();
const Percentage = z.number()
.min(0)
.max(100)
.brand<'Percentage'>();
function calculateDiscount(
price: number,
discount: z.infer<typeof Percentage>
): number {
return price * (1 - discount / 100);
}
const discount = Percentage.parse(20);
calculateDiscount(100, discount); // OK
calculateDiscount(100, 20); // Type error
Domain-Driven Design
const Email = z.string().email().brand<'Email'>();
const Username = z.string().min(3).brand<'Username'>();
const Password = z.string().min(8).brand<'Password'>();
const User = z.object({
id: z.string().brand<'UserId'>(),
email: Email,
username: Username,
});
type User = z.infer<typeof User>;
// {
// id: string & Brand<'UserId'>;
// email: string & Brand<'Email'>;
// username: string & Brand<'Username'>;
// }
function sendEmail(to: z.infer<typeof Email>) {
// Type system ensures you can't accidentally pass a username or plain string
}
Validated vs Unvalidated Data
const ValidatedInput = z.string()
.min(1)
.brand<'Validated'>();
function processData(data: z.infer<typeof ValidatedInput>) {
// data is guaranteed to be validated
console.log(data.toUpperCase());
}
// This won't compile - string is not branded
// processData('raw string');
// Must validate first
const validated = ValidatedInput.parse('raw string');
processData(validated); // OK
Brand Type Helper
Zod exports the Brand type helper for type annotations:
import { z, type Brand } from 'zod';
// Using the helper type
type UserId = string & Brand<'UserId'>;
// Or using the $brand symbol
type ProductId = string & { [z.$brand]: { ProductId: true } };
Runtime Behavior
Brands have no runtime overhead:
const UserId = z.string().brand<'UserId'>();
const id = UserId.parse('user-123');
console.log(id); // 'user-123'
console.log(typeof id); // 'string'
console.log(id === 'user-123'); // true
JSON.stringify(id); // '"user-123"'
The brand exists only in the type system - at runtime it’s just the base value.
Limitations
Brands are erased at runtime. They only provide compile-time type safety in TypeScript.
const UserId = z.string().brand<'UserId'>();
const ProductId = z.string().brand<'ProductId'>();
const userId = UserId.parse('123');
const productId = ProductId.parse('123');
// At runtime, these are equal
console.log(userId === productId); // true
// But TypeScript treats them as different types
const test: typeof userId = productId; // Type error
Combining Brands with Validation
// Brand represents a validated email
const Email = z.string()
.email()
.brand<'ValidatedEmail'>();
// Any value of this type has been validated
function sendEmail(to: z.infer<typeof Email>) {
// No need to re-validate, type system guarantees it
}
// Create validated email
const email = Email.parse('[email protected]');
sendEmail(email); // OK
// Can't pass unvalidated string
sendEmail('[email protected]'); // Type error