Композиция сложных схем

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

Основой служит комбинирование объектов и массивов через object() и array(). Каждая схема описывает самостоятельный фрагмент структуры данных, который затем включается в более крупную модель.

import * as Yup from 'yup';

const nameSchema = Yup.object({
  firstName: Yup.string().required(),
  lastName: Yup.string().required()
});

const contactSchema = Yup.object({
  email: Yup.string().email().required(),
  phone: Yup.string()
});

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

Объединение схем через concat

Метод concat позволяет последовательно объединять схемы одного типа, расширяя их правилами из другой схемы. Это особенно полезно при наращивании валидации без изменения исходной логики.

const baseSchema = Yup.object({
  id: Yup.number().required()
});

const extendedSchema = baseSchema.concat(
  Yup.object({
    createdAt: Yup.date().required()
  })
);

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

Вложенные структуры

Сложные схемы часто содержат глубоко вложенные объекты. Yup поддерживает рекурсивную композицию через object.shape, где каждый уровень может включать собственные схемы.

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

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

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

Для массивов используется array().of(), где внутренняя схема задаёт структуру элементов:

const orderSchema = Yup.object({
  items: Yup.array().of(
    Yup.object({
      productId: Yup.number().required(),
      quantity: Yup.number().min(1).required()
    })
  )
});

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

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

const schema = Yup.object({
  type: Yup.string().required(),
  companyName: Yup.string().when('type', {
    is: 'business',
    then: schema => schema.required(),
    otherwise: schema => schema.notRequired()
  })
});

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

Условные схемы могут ссылаться на несколько полей одновременно:

const schema = Yup.object({
  password: Yup.string(),
  confirmPassword: Yup.string().when('password', (password, schema) =>
    password ? schema.required().oneOf([Yup.ref('password')]) : schema
  )
});

Переиспользование и модульность

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

export const timestampFields = {
  createdAt: Yup.date().required(),
  updatedAt: Yup.date().required()
};

export const baseEntitySchema = Yup.object({
  id: Yup.number().required(),
  ...timestampFields
});

Распространённый приём — композиция через spread-оператор, где отдельные блоки схем объединяются в единый объект перед передачей в object().

Динамические схемы lazy

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

const dynamicSchema = Yup.lazy(value => {
  if (typeof value === 'string') {
    return Yup.string().min(3);
  }
  if (typeof value === 'number') {
    return Yup.number().min(10);
  }
  return Yup.mixed();
});

Этот механизм особенно важен при работе с API, где формат данных может меняться в зависимости от условий или версии ответа.

Расширение и наследование схем

Хотя в Yup нет классического наследования, поведение схожее достигается через композицию и повторное использование базовых схем.

const baseUser = Yup.object({
  username: Yup.string().required(),
  role: Yup.string().required()
});

const adminUser = baseUser.concat(
  Yup.object({
    permissions: Yup.array().of(Yup.string()).required()
  })
);

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

Дополнительное расширение возможно через функцию-генератор:

const createUserSchema = (extraFields = {}) =>
  Yup.object({
    username: Yup.string().required(),
    ...extraFields
  });

const extended = createUserSchema({
  email: Yup.string().email().required()
});

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

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

const tagSchema = Yup.object({
  name: Yup.string().required()
});

const schema = Yup.object({
  tags: Yup.array().of(tagSchema)
});

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

const baseItem = Yup.object({
  id: Yup.number().required()
});

const detailedItem = baseItem.concat(
  Yup.object({
    description: Yup.string()
  })
);

const schema = Yup.object({
  items: Yup.array().of(detailedItem)
});

Глубокая композиция и структурные паттерны

При проектировании сложных форм и API-валидаторов часто используется принцип слоёв:

  • базовые примитивы (string, number, boolean)
  • доменные блоки (user, address, product)
  • агрегированные схемы (profile, order, dashboard)

Каждый слой строится через композицию предыдущего, формируя дерево зависимостей.

const product = Yup.object({
  id: Yup.number(),
  title: Yup.string()
});

const cartItem = Yup.object({
  product,
  quantity: Yup.number().min(1)
});

const cart = Yup.object({
  items: Yup.array().of(cartItem)
});

Композиция через трансформации

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

const schema = Yup.string().transform(value =>
  value ? value.trim() : value
);

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

Изоляция и повторное использование фрагментов

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

const authFields = {
  email: Yup.string().email().required(),
  password: Yup.string().min(8).required()
};

const profileFields = {
  firstName: Yup.string(),
  lastName: Yup.string()
};

const schema = Yup.object({
  ...authFields,
  ...profileFields
});

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