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

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

В основе Superstruct лежит функция создания структур, которая возвращает валидатор:

import { struct } from 'superstruct'

const User = struct({
  id: 'number',
  name: 'string',
})

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

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

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

Самый простой уровень композиции — вложенные объекты. Один struct используется внутри другого:

import { struct } from 'superstruct'

const Address = struct({
  city: 'string',
  zip: 'number',
})

const User = struct({
  name: 'string',
  address: Address,
})

Здесь Address становится частью более сложной схемы. Такой подход позволяет:

  • изолировать ответственность схем
  • переиспользовать общие фрагменты
  • снижать дублирование логики

Расширение схем через объединение

Для композиции на уровне расширения объектов используется паттерн объединения структур.

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

const BaseUser = struct({
  id: 'number',
  name: 'string',
})

const AdminUser = struct({
  ...BaseUser.schema,
  role: 'string',
})

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

Intersection: пересечение структур

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

import { intersection, struct } from 'superstruct'

const HasId = struct({
  id: 'number',
})

const HasTimestamps = struct({
  createdAt: 'number',
  updatedAt: 'number',
})

const Entity = intersection([HasId, HasTimestamps])

Такой подход полезен, когда:

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

Union: выбор одной из схем

Композиция не всегда означает объединение. Часто требуется выбор одного из вариантов.

import { union, struct } from 'superstruct'

const Cat = struct({
  type: 'string',
  meows: 'boolean',
})

const Dog = struct({
  type: 'string',
  barks: 'boolean',
})

const Pet = union([Cat, Dog])

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

  • альтернативных форм данных
  • полиморфных структур
  • API-ответов с разными типами

Валидация проходит успешно, если соответствует хотя бы одной схеме.

Опциональные поля как элемент композиции

Опциональность позволяет постепенно усложнять структуру без разрушения базового контракта.

import { struct, optional } from 'superstruct'

const User = struct({
  name: 'string',
  age: optional('number'),
})

Композиционно это важно тем, что позволяет:

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

Значения по умолчанию

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

import { struct, defaulted } from 'superstruct'

const Settings = struct({
  theme: defaulted('string', 'light'),
})

Такие схемы позволяют:

  • описывать поведение данных, а не только форму
  • снижать нагрузку на прикладной код
  • централизовать бизнес-правила

Преобразование данных в композиции

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

import { struct, coerce } from 'superstruct'

const NumberFromString = coerce('number', 'string', (value) =>
  Number(value)
)

Это позволяет строить цепочки:

  • вход → преобразование → валидация → результат

Такая модель особенно полезна при работе с внешними API и пользовательским вводом.

Рекурсивная композиция

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

import { struct } from 'superstruct'

const Node = struct.lazy(() =>
  struct({
    value: 'string',
    children: struct.array(Node),
  })
)

Это фундамент для:

  • деревьев
  • вложенных меню
  • графоподобных структур

Композиция здесь становится бесконечно расширяемой.

Композиция через функции-обёртки

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

const withMeta = (base) =>
  struct({
    ...base.schema,
    meta: 'object',
  })

const User = withMeta(
  struct({
    name: 'string',
  })
)

Так формируются «паттерны схем», которые можно применять к разным моделям.

Частичное переиспользование схем

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

const Auth = struct({
  email: 'string',
  password: 'string',
})

const Login = struct({
  email: Auth.schema.email,
  password: Auth.schema.password,
})

Такой подход позволяет:

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

Композиция валидации через refine

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

import { struct, refine } from 'superstruct'

const PositiveNumber = refine('number', 'PositiveNumber', (value) => {
  return value > 0
})

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

Слоистая модель композиции

В реальных приложениях схемы редко существуют изолированно. Они образуют слои:

  1. Базовые типы (string, number)
  2. Примитивные структуры (struct)
  3. Комбинации (union, intersection)
  4. Модификаторы (optional, defaulted)
  5. Трансформации (coerce)
  6. Бизнес-валидация (refine)

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

Композиция как архитектурный инструмент

Использование композиции схем позволяет выстраивать архитектуру данных, в которой:

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

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