Cross-field validation

Валидация отдельных полей часто недостаточна при работе со сложными структурами данных. Реальные бизнес-правила почти всегда затрагивают несколько значений одновременно: подтверждение пароля, проверка диапазонов дат, условная обязательность полей, согласованность связанных параметров. Подобные сценарии реализуются через cross-field validation — проверку взаимосвязей между полями одного объекта.

В библиотеке Superstruct подобная логика строится поверх базовых структур с использованием механизмов композиции и пользовательских проверок. Основной инструмент — refine, позволяющий накладывать дополнительные ограничения на уже определённую структуру.


Базовая модель перекрёстной проверки

Любая cross-field validation начинается с определения базовой структуры объекта. На этом уровне описываются типы данных без бизнес-логики взаимосвязей.

import { object, string } from 'superstruct'

const UserCredentials = object({
  password: string(),
  confirmPassword: string(),
})

На данном этапе проверяется только корректность типов. Однако логика согласованности полей отсутствует.


Использование refine для проверки зависимых полей

Функция refine расширяет существующую структуру дополнительным условием, которое получает весь объект целиком. Это ключевой механизм для cross-field validation.

import { refine } from 'superstruct'

const PasswordMatch = refine(
  UserCredentials,
  'PasswordMatch',
  (value) => value.password === value.confirmPassword,
  'Пароли должны совпадать'
)

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


Проверка диапазонов значений

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

import { object, string, refine } from 'superstruct'

const DateRange = refine(
  object({
    startDate: string(),
    endDate: string(),
  }),
  'DateRange',
  (value) => new Date(value.startDate) <= new Date(value.endDate),
  'Начальная дата не может быть позже конечной'
)

Здесь логика выходит за рамки локальной проверки одного поля и затрагивает семантику всей структуры.


Сложные бизнес-правила между полями

В реальных приложениях часто встречаются зависимости вида «если одно поле заполнено, другое становится обязательным» или «значение зависит от выбранного режима».

import { object, string, optional, refine } from 'superstruct'

const Profile = refine(
  object({
    role: string(),
    adminCode: optional(string()),
  }),
  'ProfileRoleRule',
  (value) => {
    if (value.role === 'admin') {
      return typeof value.adminCode === 'string'
    }
    return true
  },
  'Для роли admin требуется adminCode'
)

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


Вложенные структуры и перекрёстная логика

Cross-field validation сохраняет свою применимость и при работе с вложенными объектами. В этом случае проверка может затрагивать как поля внутри одного уровня, так и значения из разных вложенных структур.

import { object, number, refine } from 'superstruct'

const Order = refine(
  object({
    itemsCount: number(),
    maxAllowed: number(),
  }),
  'OrderLimit',
  (value) => value.itemsCount <= value.maxAllowed,
  'Превышено допустимое количество товаров'
)

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


Композиция структур и многоуровневая валидация

Superstruct позволяет комбинировать базовые структуры с последующим добавлением cross-field правил. Это создаёт многоуровневую систему проверок: сначала типизация, затем локальные ограничения, затем межполевые зависимости.

import { object, string, size, refine } from 'superstruct'

const BaseAccount = object({
  username: size(string(), 3, 20),
  password: size(string(), 8, 100),
})

const AccountWithRules = refine(
  BaseAccount,
  'AccountRules',
  (value) => value.username !== value.password,
  'Имя пользователя и пароль не должны совпадать'
)

Такой подход отделяет формальные ограничения от бизнес-логики.


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

При масштабировании систем часто возникает необходимость переиспользования cross-field правил. Для этого проверка выделяется в отдельные функции.

const passwordsMatch = (value) =>
  value.password === value.confirmPassword

const PasswordStruct = refine(
  object({
    password: string(),
    confirmPassword: string(),
  }),
  'PasswordsMatch',
  passwordsMatch,
  'Пароли должны совпадать'
)

Выделение предикатов повышает тестируемость и упрощает композицию логики.


Динамическая логика в зависимости от состояния объекта

Cross-field validation может учитывать сразу несколько условий, формируя сложные зависимости между полями.

const Config = refine(
  object({
    mode: string(),
    timeout: number(),
    retries: number(),
  }),
  'ConfigRules',
  (value) => {
    if (value.mode === 'strict') {
      return value.retries <= 3 && value.timeout >= 1000
    }
    return true
  },
  'Некорректные параметры для режима strict'
)

Здесь проверка становится частью бизнес-модели, а не просто валидацией типов.


Ошибки и диагностика в cross-field validation

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

import { validate } from 'superstruct'

const [error, result] = validate(
  { password: '123', confirmPassword: '321' },
  PasswordMatch
)

Ошибка в таком случае отражает нарушение общего правила, а не локального ограничения.

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


Ограничения подхода refine в cross-field сценариях

Несмотря на гибкость, refine обладает рядом особенностей:

  • отсутствие автоматического связывания ошибок с конкретными полями
  • необходимость ручного описания сложных зависимостей
  • рост сложности предикатов при увеличении числа условий
  • отсутствие встроенного механизма декларативных зависимостей между полями

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


Комбинирование cross-field validation с композициями структур

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

const Address = object({
  country: string(),
  city: string(),
})

const User = refine(
  object({
    name: string(),
    address: Address,
  }),
  'UserAddressRule',
  (value) => value.address.country !== '' && value.address.city !== '',
  'Адрес должен быть заполнен'
)

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


Практическое применение cross-field validation

Cross-field validation используется в следующих типах задач:

  • проверка регистрационных форм (пароли, подтверждения, роли)
  • финансовые расчёты (баланс, лимиты, комиссии)
  • фильтры поиска (диапазоны дат и чисел)
  • конфигурационные системы (режимы и зависимости параметров)
  • формы заказа (количество, доступность, ограничения)

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


Расширение логики через пользовательские структуры

В более сложных сценариях вместо refine создаются собственные структуры, инкапсулирующие cross-field логику.

import { Struct } from 'superstruct'

class RangeStruct extends Struct {
  constructor() {
    super('Range', (value) => {
      return value.start <= value.end
    })
  }
}

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