Валидация отдельных полей часто недостаточна при работе со сложными структурами данных. Реальные бизнес-правила почти всегда затрагивают несколько значений одновременно: подтверждение пароля, проверка диапазонов дат, условная обязательность полей, согласованность связанных параметров. Подобные сценарии реализуются через cross-field validation — проверку взаимосвязей между полями одного объекта.
В библиотеке Superstruct подобная логика строится поверх базовых
структур с использованием механизмов композиции и пользовательских
проверок. Основной инструмент — refine, позволяющий
накладывать дополнительные ограничения на уже определённую
структуру.
Любая cross-field validation начинается с определения базовой структуры объекта. На этом уровне описываются типы данных без бизнес-логики взаимосвязей.
import { object, string } from 'superstruct'
const UserCredentials = object({
password: string(),
confirmPassword: string(),
})
На данном этапе проверяется только корректность типов. Однако логика согласованности полей отсутствует.
Функция 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'
)
Здесь проверка становится частью бизнес-модели, а не просто валидацией типов.
При использовании refine ошибка формируется на уровне
всей структуры. Это означает, что результат не привязан к конкретному
полю, если дополнительно не реализована кастомная логика распределения
ошибок.
import { validate } from 'superstruct'
const [error, result] = validate(
{ password: '123', confirmPassword: '321' },
PasswordMatch
)
Ошибка в таком случае отражает нарушение общего правила, а не локального ограничения.
Для более точной диагностики часто добавляется дополнительная логика анализа внутри предиката или используются составные структуры с промежуточными проверками.
Несмотря на гибкость, refine обладает рядом
особенностей:
При усложнении логики структура проверок может превращаться в набор императивных условий, что требует аккуратной организации кода.
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 используется в следующих типах задач:
Во всех случаях ключевая задача заключается в контроле согласованности данных, а не только их формальной корректности.
В более сложных сценариях вместо refine создаются
собственные структуры, инкапсулирующие cross-field логику.
import { Struct } from 'superstruct'
class RangeStruct extends Struct {
constructor() {
super('Range', (value) => {
return value.start <= value.end
})
}
}
Такой подход используется для повторного применения сложных правил валидации в разных частях приложения.