При построении сложных форм в JavaScript-экосистеме центральной проблемой становится управление валидационными правилами. Библиотека Yup предоставляет декларативный способ описания схем, а YupResolver выступает адаптером между схемой и системами управления формами, такими как react-hook-form. В основе масштабируемости лежат механизмы композиции и наследования схем, позволяющие избегать дублирования и формировать предсказуемую структуру валидации.
Композиция в Yup опирается на идею объединения небольших, переиспользуемых фрагментов схем в более крупные структуры. Наследование в классическом смысле отсутствует, но его поведение моделируется через расширение объектов, объединение схем и условные конструкции.
Любая схема Yup начинается с примитивного описания поля:
import * as yup from 'yup';
const emailSchema = yup.string()
.email('Некорректный email')
.required('Email обязателен');
const passwordSchema = yup.string()
.min(8, 'Минимум 8 символов')
.required('Пароль обязателен');
Такие схемы выступают атомарными единицами композиции. Их основная ценность заключается в независимости: каждая схема может быть использована в разных контекстах без изменений.
Yup позволяет формировать объектные схемы через
object().shape(), где ключи могут ссылаться на ранее
определённые фрагменты:
const loginSchema = yup.object().shape({
email: emailSchema,
password: passwordSchema,
});
Такой подход формирует первую ступень композиции: повторное использование логики валидации без копирования. При увеличении числа форм этот подход снижает количество дублируемого кода и упрощает поддержку.
Одним из распространённых способов «наследования» является расширение
объектной схемы через объединение concat.
const baseUserSchema = yup.object().shape({
email: emailSchema,
});
const extendedUserSchema = baseUserSchema.concat(
yup.object().shape({
password: passwordSchema,
})
);
Механизм concat работает как поверхностное объединение
схем объектов. При совпадении ключей применяется правило приоритета
второй схемы, что позволяет переопределять поведение.
Этот подход используется для построения базовых моделей (например, пользователь, профиль, адрес), которые затем расширяются в зависимости от контекста: регистрация, редактирование профиля, админ-панель.
При увеличении сложности структуры данных возникает необходимость вложенной композиции:
const addressSchema = yup.object().shape({
city: yup.string().required(),
street: yup.string().required(),
});
const profileSchema = yup.object().shape({
name: yup.string().required(),
address: addressSchema,
});
Здесь формируется иерархия схем, где каждая вложенная структура
остаётся автономной. Такой подход обеспечивает локализацию изменений:
изменение addressSchema автоматически отражается во всех
родительских схемах.
Хотя Yup не реализует наследование классов, базовые схемы часто используются как эквивалент родительских классов:
const baseSchema = {
createdAt: yup.date().default(() => new Date()),
updatedAt: yup.date().default(() => new Date()),
};
const entitySchema = yup.object().shape({
...baseSchema,
id: yup.string().required(),
});
Использование spread-оператора создаёт поверхностное расширение, имитируя наследование структуры. Такой подход особенно полезен для доменных моделей, где существует общая метаинформация.
Одним из ключевых механизмов гибкой архитектуры является условная
логика через when.
const schema = yup.object().shape({
isCompany: yup.boolean(),
companyName: yup.string().when('isCompany', {
is: true,
then: schema => schema.required(),
otherwise: schema => schema.notRequired(),
}),
});
Условная композиция позволяет формировать динамическую структуру схемы, адаптирующуюся к состоянию данных. В контексте больших форм это заменяет необходимость создавать несколько отдельных схем.
Для сценариев, где структура данных неизвестна заранее, используется
lazy:
const dynamicSchema = yup.lazy(value => {
if (typeof value === 'string') {
return yup.string().min(3);
}
if (typeof value === 'number') {
return yup.number().min(0);
}
return yup.mixed();
});
Ленивая схема позволяет реализовать поведение, близкое к полиморфизму: тип валидации определяется в момент исполнения. Это расширяет возможности композиции за пределы статических структур.
В архитектуре форм часто выделяются доменные блоки: пользователь, платежные данные, настройки. Каждый блок оформляется как независимая схема:
const paymentSchema = yup.object().shape({
cardNumber: yup.string().required(),
expiry: yup.string().required(),
});
const settingsSchema = yup.object().shape({
theme: yup.string(),
notifications: yup.boolean(),
});
Композиция на уровне формы:
const formSchema = yup.object().shape({
user: profileSchema,
payment: paymentSchema,
settings: settingsSchema,
});
Такой подход позволяет рассматривать форму как агрегат независимых доменных моделей, каждая из которых может эволюционировать отдельно.
YupResolver выступает связующим звеном между схемой и механизмом валидации формы:
import { yupResolver } from '@hookform/resolvers/yup';
const resolver = yupResolver(formSchema);
При использовании сложных композиционных схем resolver не изменяет поведение валидации, но наследует всю структуру Yup-модели. Это означает, что любые изменения в композиции схем автоматически отражаются в логике проверки формы без дополнительной конфигурации.
Важным аспектом становится предсказуемость: resolver работает исключительно как интерпретатор схемы, а не как трансформатор логики.
Для масштабирования архитектуры применяется паттерн фабрик:
const createEmailSchema = ({ required = true } = {}) => {
let schema = yup.string().email();
if (required) {
schema = schema.required();
}
return schema;
};
Фабрики позволяют параметризовать поведение схем, создавая контролируемую вариативность. Это особенно важно в системах, где одна и та же сущность используется в разных бизнес-контекстах.
При проектировании больших приложений схемы часто выносятся в отдельные модули:
schemas/
email.js
password.js
user.js
address.js
Каждый модуль экспортирует автономную схему, которая затем используется в других частях системы. Это формирует слой абстракции, аналогичный доменным сервисам.
Отсутствие классического наследования в Yup приводит к нескольким ограничениям:
Эти ограничения компенсируются следующими механизмами:
concat для расширенияwhen для условной логикиКомбинация этих подходов формирует гибкую систему, эквивалентную наследованию в декларативной форме.
При использовании TypeScript композиция схем приобретает дополнительный уровень строгости. Типы могут быть выведены из схемы:
import { InferType } from 'yup';
type User = InferType<typeof profileSchema>;
Композиция схем напрямую влияет на итоговую типовую модель. Изменение любого фрагмента схемы автоматически распространяется на весь тип, что снижает риск рассинхронизации данных и валидации.
Композиционные схемы часто используются как единый источник истины для валидации:
Один и тот же набор схем может импортироваться в разные среды, обеспечивая консистентность правил. Это особенно важно в распределённых системах, где различие в логике валидации приводит к рассинхронизации поведения интерфейса и backend.
В доменно-ориентированных структурах схемы начинают отражать бизнес-логику:
Композиция позволяет моделировать такие структуры без нарушения целостности схем. Каждый элемент остаётся независимым, но участвует в общей структуре через вложенность и расширение.
При достижении критической сложности схема разбивается на уровни:
Такое разделение снижает когнитивную нагрузку и упрощает сопровождение. Yup становится инструментом композиции, а не монолитной схемой валидации.
Стабильные системы валидации строятся на принципе максимального переиспользования схем. Любая логика, вынесенная в отдельный модуль, становится точкой стабильности. Изменение такой точки автоматически отражается во всех местах использования, что требует аккуратного проектирования границ ответственности между схемами.
Композиция и псевдонаследование формируют основу для построения предсказуемых и расширяемых систем валидации, где YupResolver выступает неизменным интерфейсом между декларативной моделью данных и механизмом обработки форм.