Композиция валидаторов

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

Каждый валидатор в Superstruct — это функция, которая принимает значение и либо возвращает его в нормализованном виде, либо выбрасывает ошибку. Композиция строится вокруг идеи последовательного применения таких функций:

  1. базовая структура задаёт форму данных;
  2. уточнения добавляют ограничения;
  3. трансформеры изменяют входные значения;
  4. комбинирующие структуры объединяют несколько валидаторов в один.

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


Комбинирование через object

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

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

const User = object({
  name: string(),
  age: number(),
})

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

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


Использование partial для частичной композиции

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

import { partial } from 'superstruct'

const OptionalUser = partial(User)

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


Расширение через extend

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

import { extend, string } from 'superstruct'

const BaseUser = object({
  name: string(),
})

const AdminUser = extend(BaseUser, {
  role: string(),
})

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


Пересечение структур (intersection)

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

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

const HasName = object({
  name: string(),
})

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

const Person = intersection([HasName, HasAge])

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


Объединение альтернатив (union)

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

import { union, string, number } from 'superstruct'

const StringOrNumber = union([string(), number()])

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

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


Ленивые структуры (lazy) и рекурсия

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

import { lazy, object, array, string } from 'superstruct'

const Node = lazy(() =>
  object({
    value: string(),
    children: array(Node),
  })
)

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


Модификация через refine

Композиция валидаторов часто требует добавления бизнес-логики. Для этого используется refine, который накладывает дополнительные ограничения поверх базовой структуры.

import { refine, string } from 'superstruct'

const EvenLengthString = refine(string(), 'EvenLengthString', (value) => {
  return value.length % 2 === 0
})

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


Трансформации через coerce

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

import { coerce, number, string } from 'superstruct'

const NumberFromString = coerce(number(), string(), (value) => {
  return Number(value)
})

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


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

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

const Name = string()

const User = object({
  name: Name,
})

const Comment = object({
  author: Name,
})

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


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

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

const Post = object({
  title: string(),
  author: object({
    name: string(),
    id: number(),
  }),
  tags: array(string()),
})

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


Комбинация трансформаций и проверок

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

const TrimmedString = coerce(
  refine(string(), 'TrimmedString', (v) => v.length > 0),
  string(),
  (value) => value.trim()
)

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


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

При использовании Superstruct валидаторы перестают быть изолированными функциями проверки и превращаются в элементы модели данных. Их композиция позволяет:

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

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