Intersection

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

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

Пересечение представляет собой операцию логического «И» над наборами правил. Если одна структура проверяет тип и формат, а другая — диапазон значений или наличие дополнительных полей, итоговая структура требует соблюдения всех этих условий одновременно.

При проверке значения происходит последовательное применение всех структур. Любое несоответствие приводит к ошибке валидации. При этом ошибки могут агрегироваться, формируя комплексное описание причин несоответствия.

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

Базовый синтаксис

В типичном использовании пересечение создаётся через функцию композиции:

import { intersection, string, number, object } from 'superstruct'

const IdStruct = string()
const TimestampStruct = number()

const CompositeStruct = intersection([
  IdStruct,
  TimestampStruct,
])

Однако на практике чаще пересекаются объектные структуры, так как именно они содержат независимые группы правил:

const UserBase = object({
  id: string(),
})

const UserMeta = object({
  age: number(),
})

Комбинация этих структур:

const UserStruct = intersection([UserBase, UserMeta])

Результат требует наличия обоих наборов свойств.

Пересечение объектных структур

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

const BaseProfile = object({
  id: string(),
  name: string(),
})

const ContactInfo = object({
  email: string(),
})

Композиция:

const FullProfile = intersection([
  BaseProfile,
  ContactInfo,
])

Итоговая структура требует одновременного наличия id, name и email.

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

Поведение при конфликтующих правилах

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

const A = string()
const B = number()

const Impossible = intersection([A, B])

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

При работе с объектами конфликты проявляются на уровне свойств:

const A = object({
  id: string(),
})

const B = object({
  id: number(),
})

const Conflict = intersection([A, B])

Поле id должно одновременно соответствовать двум несовместимым типам.

Глубокое объединение ограничений

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

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

const Geo = object({
  city: string(),
  lat: number(),
  lon: number(),
})

const Location = intersection([Address, Geo])

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

Композиция нескольких уровней

Пересечения могут включать более двух структур, формируя цепочки ограничений:

const A = object({ a: string() })
const B = object({ b: number() })
const C = object({ c: boolean() })

const Combined = intersection([A, B, C])

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

Поведение при валидации

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

При проверке объектов:

  1. Применяется первая структура
  2. Результат передаётся во вторую
  3. Последовательно проверяются все остальные
  4. Ошибки агрегируются

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

Пересечения и расширение схем

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

const Base = object({
  id: string(),
})

const WithTimestamps = object({
  createdAt: number(),
  updatedAt: number(),
})

const Extended = intersection([Base, WithTimestamps])

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

Повторяющиеся поля

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

const A = object({
  value: string(),
})

const B = object({
  value: string(),
})

В этом случае конфликт отсутствует, и поле сохраняет единый тип.

Если же ограничения различаются:

const A = object({
  value: string(),
})

const B = object({
  value: pattern(/[0-9]+/),
})

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

Использование с примитивами и объектами

Пересечения могут включать не только объекты, но и примитивные структуры:

const Positive = number()
const Integer = number()

const StrictNumber = intersection([Positive, Integer])

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

Ошибки валидации и их структура

Ошибки при пересечении содержат информацию о каждом слое проверки. Это позволяет точно определить, какая из структур не прошла проверку.

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

Производительность пересечений

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

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

Использование в модульных схемах

Пересечение особенно эффективно в модульных архитектурах, где каждая структура описывает отдельный аспект данных:

  • идентификационные данные
  • метаданные
  • бизнес-правила
  • технические ограничения

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

const Identity = object({
  id: string(),
})

const Metadata = object({
  createdAt: number(),
})

const Permissions = object({
  role: string(),
})

const Entity = intersection([Identity, Metadata, Permissions])

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