Композиция схем в Zod является ключевым механизмом построения масштабируемых и переиспользуемых моделей данных, позволяя собирать сложные структуры из более простых блоков без дублирования логики и с сохранением строгой типизации в TypeScript.
В основе подхода лежит идея декомпозиции: каждая схема описывает минимально возможную сущность, после чего эти сущности комбинируются в более сложные структуры. Такой подход снижает связность кода и упрощает сопровождение.
import { z } from "zod";
const UserId = z.string().uuid();
const Email = z.string().email();
const UserBase = z.object({
id: UserId,
email: Email,
});
Повторное использование базовых схем позволяет формировать единый источник истины для отдельных полей и их валидации.
Одним из основных механизмов композиции выступает расширение
объектных схем. Метод extend добавляет новые поля к
существующей структуре, сохраняя уже определённые правила валидации.
const UserWithName = UserBase.extend({
name: z.string().min(1),
});
Расширение применяется в случаях, когда требуется постепенное наращивание модели без модификации исходного определения.
Метод merge используется для объединения двух объектных
схем в одну. В отличие от расширения, объединение предполагает
симметричное слияние структур.
const Timestamp = z.object({
createdAt: z.date(),
updatedAt: z.date(),
});
const UserWithTimestamp = UserBase.merge(Timestamp);
При совпадении ключей приоритет определяется порядком объединения, что требует аккуратного проектирования базовых схем.
Функция пересечения позволяет комбинировать произвольные схемы, включая не только объекты, но и более общие типы. Результирующая схема должна удовлетворять всем условиям одновременно.
const A = z.object({ a: z.string() });
const B = z.object({ b: z.number() });
const AB = z.intersection(A, B);
Пересечение используется при необходимости строгого наложения ограничений из нескольких независимых источников.
Композиция через объединение (union) позволяет описывать
альтернативные структуры данных, где валидным считается один из
вариантов.
const Success = z.object({
status: z.literal("success"),
data: z.string(),
});
const Error = z.object({
status: z.literal("error"),
message: z.string(),
});
const Response = z.union([Success, Error]);
Для более строгой и эффективной проверки применяется дискриминированное объединение:
const Response = z.discriminatedUnion("status", [Success, Error]);
Дискриминатор снижает стоимость проверки, так как выбор схемы происходит по фиксированному полю.
Композиция схем включает не только объединение, но и преобразование
данных. Метод transform позволяет менять форму результата
после валидации.
const StringNumber = z.string().transform((val) => Number(val));
Преобразование часто применяется для адаптации внешних данных к внутренним моделям.
Механизм pipe реализует последовательную композицию
схем, где результат одной схемы становится входом другой.
const ParsedNumber = z.string()
.pipe(z.coerce.number())
.pipe(z.number().int());
Такой подход формирует декларативный конвейер обработки данных, объединяющий парсинг и проверку.
Функция refine добавляет пользовательские правила поверх
существующей схемы, не нарушая её базовую структуру.
const PositiveNumber = z.number().refine((val) => val > 0);
Более сложная логика реализуется через superRefine, где
доступен контекст ошибок:
const Password = z.string().superRefine((val, ctx) => {
if (val.length < 8) {
ctx.addIssue({
code: z.ZodIssueCode.too_small,
minimum: 8,
type: "string",
inclusive: true,
message: "Слишком короткий пароль",
});
}
});
Метод partial преобразует все поля объекта в
необязательные, что часто используется для операций обновления.
const UserUpdate = UserBase.partial();
Комбинация partial с extend позволяет
строить модели патчей:
const UserPatch = UserBase.partial().extend({
lastModifiedBy: z.string(),
});
Композиция схем также реализуется через проекцию структуры.
const PublicUser = UserBase.pick({
id: true,
email: true,
});
Исключение полей:
const SafeUser = UserBase.omit({
email: true,
});
Подход применяется для формирования различных представлений одной и той же сущности.
Композиция становится невозможной без поддержки рекурсивных структур.
Для этого используется lazy.
const Category = z.lazy(() =>
z.object({
name: z.string(),
children: z.array(Category).optional(),
})
);
Ленивая инициализация разрывает циклические зависимости и позволяет строить деревья произвольной глубины.
На практике формируются устойчивые шаблоны построения схем:
merge и extend вместо
дублирования;discriminatedUnion для контрактов
API;pipe.Такой подход формирует предсказуемую архитектуру описания данных, в которой каждая новая схема является результатом композиции уже существующих примитивов, а не самостоятельной изолированной сущностью.