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

При работе с формами в прикладных JavaScript-приложениях схемы валидации быстро разрастаются и начинают повторяться между экранами. Разбиение схем на независимые части позволяет управлять сложностью и повторным использованием логики валидации без дублирования.

Базовый принцип — выделение устойчивых фрагментов схемы в отдельные модули:

import * as Yup from "yup";

export const emailField = Yup.string()
  .email("Некорректный формат email")
  .required("Email обязателен");

export const passwordField = Yup.string()
  .min(8, "Минимум 8 символов")
  .required("Пароль обязателен");

Такие фрагменты становятся строительными блоками для более сложных объектов. В контексте YupResolver это особенно важно, так как резолвер работает с итоговой схемой целиком, а не с её частями.


Композиция через object().shape

Основной способ сборки схемы в Yup — использование object().shape(). Он позволяет комбинировать ранее определённые поля в единый объект.

import * as Yup from "yup";
import { emailField, passwordField } from "./fields";

export const loginSchema = Yup.object().shape({
  email: emailField,
  password: passwordField,
});

Композиция здесь линейная: каждый ключ объекта соответствует независимой валидационной цепочке. При передаче этой схемы в YupResolver она интерпретируется как единое правило валидации формы.

import { yupResolver } from "@hookform/resolvers/yup";
import { useForm } from "react-hook-form";

const form = useForm({
  resolver: yupResolver(loginSchema),
});

Повторное использование схем как строительных блоков

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

export const addressSchema = Yup.object().shape({
  city: Yup.string().required(),
  street: Yup.string().required(),
  zip: Yup.string().required(),
});

Эта схема может быть встроена в другие схемы как вложенный объект:

export const userSchema = Yup.object().shape({
  name: Yup.string().required(),
  address: addressSchema,
});

При таком подходе YupResolver корректно обрабатывает вложенные ошибки, возвращая структуру ошибок, соответствующую форме данных.


Вложенная композиция и структурирование данных

При сложных формах данные часто имеют глубоко вложенную структуру. Yup поддерживает композицию через вложенные object() и array().

export const orderSchema = Yup.object().shape({
  customer: Yup.object().shape({
    name: Yup.string().required(),
    email: Yup.string().email().required(),
  }),
  items: Yup.array().of(
    Yup.object().shape({
      productId: Yup.string().required(),
      quantity: Yup.number().min(1).required(),
    })
  ),
});

Такая структура напрямую отражается в модели данных и сохраняется при обработке через YupResolver, что упрощает интеграцию с react-hook-form.


Расширение схем через переиспользование и композиционные функции

Схемы можно не только импортировать, но и расширять через функции-фабрики. Это позволяет создавать вариативные формы на основе базовой структуры.

const baseUserSchema = {
  name: Yup.string().required(),
  email: Yup.string().email().required(),
};

export const createUserSchema = (withPassword = false) =>
  Yup.object().shape({
    ...baseUserSchema,
    ...(withPassword
      ? { password: Yup.string().min(8).required() }
      : {}),
  });

Такой подход особенно полезен для сценариев «создание/редактирование», где часть полей может быть опциональной или отсутствовать.


Слияние схем и ограничения shallow-композиции

Yup не предоставляет полноценного механизма глубокого merge схем, поэтому композиция часто реализуется вручную через spread-операторы и повторное использование shape.

Важно учитывать, что поверхностное слияние работает только на уровне объекта:

const schemaA = Yup.object().shape({
  name: Yup.string().required(),
});

const schemaB = Yup.object().shape({
  age: Yup.number().required(),
});

const merged = Yup.object().shape({
  ...schemaA.fields,
  ...schemaB.fields,
});

Такой подход требует аккуратности: при совпадении ключей последняя схема перезапишет предыдущую.


Условная композиция через when

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

export const profileSchema = Yup.object().shape({
  role: Yup.string().required(),

  companyName: Yup.string().when("role", {
    is: "business",
    then: (schema) => schema.required(),
    otherwise: (schema) => schema.notRequired(),
  }),
});

В контексте YupResolver это позволяет описывать сложные бизнес-правила без внешней логики в форме.


Ленивые схемы и динамическая композиция

Для случаев, когда структура данных зависит от рантайм-значений, используется Yup.lazy.

const dynamicSchema = Yup.lazy((value) => {
  if (value?.type === "A") {
    return Yup.object().shape({
      fieldA: Yup.string().required(),
    });
  }

  return Yup.object().shape({
    fieldB: Yup.number().required(),
  });
});

Это расширяет композицию до уровня условного построения схемы целиком, что полезно для динамических форм и конфигурируемых интерфейсов.


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

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

const tagsSchema = Yup.array().of(
  Yup.object().shape({
    id: Yup.string().required(),
    label: Yup.string().required(),
  })
);

Композиция здесь выражается через повторяемую схему элемента, которая может быть переиспользована в других структурах:

const tagSchema = Yup.object().shape({
  id: Yup.string().required(),
  label: Yup.string().required(),
});

const postSchema = Yup.object().shape({
  tags: Yup.array().of(tagSchema),
});

Интеграция композиции схем с YupResolver

YupResolver не изменяет семантику схемы, но определяет способ её интерпретации в контексте формы. Композиция схем напрямую влияет на структуру ошибок, возвращаемых резолвером.

import { yupResolver } from "@hookform/resolvers/yup";

const form = useForm({
  resolver: yupResolver(userSchema),
});

Ошибки наследуют структуру схемы:

  • вложенные объекты → вложенные ошибки
  • массивы → ошибки по индексам
  • условные поля → динамическое появление ошибок

Это делает композицию не только архитектурным приёмом, но и способом управления структурой ошибок UI.


Переиспользуемые фабрики схем как архитектурный слой

При масштабировании приложений схемы начинают выполнять роль доменного слоя. Их композиция превращается в декларативное описание бизнес-правил.

export const createAuthSchema = ({ requirePhone }) =>
  Yup.object().shape({
    email: emailField,
    password: passwordField,
    phone: requirePhone
      ? Yup.string().required()
      : Yup.string().notRequired(),
  });

Такие фабрики позволяют изолировать вариативность правил и сохранять единый источник истины для валидации.


Композиция и границы ответственности схем

Схема перестаёт быть просто валидатором и становится частью контрактной модели данных. При композиции важно удерживать границы:

  • поле отвечает только за свою валидацию
  • объект — за структуру данных
  • фабрика — за вариативность сценариев

Нарушение этих границ приводит к разрастанию зависимостей между схемами и усложняет использование YupResolver в разных контекстах приложения.