Композиция схем

Композиция схем в 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

Механизм pipe реализует последовательную композицию схем, где результат одной схемы становится входом другой.

const ParsedNumber = z.string()
  .pipe(z.coerce.number())
  .pipe(z.number().int());

Такой подход формирует декларативный конвейер обработки данных, объединяющий парсинг и проверку.

Модификация через refine

Функция 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(),
  })
);

Ленивая инициализация разрывает циклические зависимости и позволяет строить деревья произвольной глубины.

Композиционные паттерны

На практике формируются устойчивые шаблоны построения схем:

  • выделение базовых сущностей и их повторное использование;
  • разделение моделей на read/write представления;
  • комбинирование через merge и extend вместо дублирования;
  • использование discriminatedUnion для контрактов API;
  • построение трансформационных цепочек через pipe.

Такой подход формирует предсказуемую архитектуру описания данных, в которой каждая новая схема является результатом композиции уже существующих примитивов, а не самостоятельной изолированной сущностью.