Структура данных и схемы

В Superstruct схема (struct) выступает как декларативное описание формы данных, определяющее допустимую структуру, типы и правила валидации значений. Любая схема здесь рассматривается как функция или композиция функций, которая принимает входное значение и либо приводит его к корректному виду, либо сообщает о нарушении ограничений.

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

Строки, числа и логические значения

Простейшие схемы определяют базовые типы данных:

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

string()
number()
boolean()

Каждая из этих структур проверяет соответствие значения ожидаемому типу:

  • string() — допускает только строки
  • number() — допускает только числа (исключая NaN в типичных конфигурациях)
  • boolean() — допускает true/false

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

Составные структуры и объекты

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

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

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

Такая структура фиксирует контракт данных:

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

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

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

Объекты могут включать другие объекты, формируя иерархические схемы:

const Profile = object({
  username: string(),
  stats: object({
    posts: number(),
    likes: number(),
  }),
})

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

Массивы и коллекции

Для описания повторяющихся элементов используется структура массива.

import { array, string } from 'superstruct'

const Tags = array(string())

Такое описание гарантирует:

  • все элементы являются строками
  • порядок элементов сохраняется
  • проверка применяется к каждому элементу отдельно

Массивы могут комбинироваться с объектами:

const Users = array(
  object({
    id: number(),
    name: string(),
  })
)

Перечисления и литералы

Для ограниченных наборов значений применяются перечисления.

import { enums } from 'superstruct'

const Role = enums(['admin', 'user', 'guest'])

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

Литералы позволяют задать единственное допустимое значение:

import { literal } from 'superstruct'

const ActiveFlag = literal(true)

Это полезно для строгих контрактов конфигураций или маркеров состояния.

Объединения типов

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

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

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

При валидации значение проверяется последовательно по всем вариантам, пока не найдётся совпадение.

Такой подход используется для:

  • гибких API
  • миграций данных
  • обработки неоднородных входных форматов

Опциональные и допускающие null значения

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

Опциональные поля

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

const Schema = object({
  nickname: optional(string()),
})

Поле может отсутствовать, но если оно присутствует — обязано соответствовать типу.

Nullable значения

import { nullable, number } from 'superstruct'

const MaybeNumber = nullable(number())

Допускается null либо корректное значение указанного типа.

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

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

import { defaulted, string } from 'superstruct'

const Name = defaulted(string(), 'unknown')

Если значение отсутствует, оно заменяется на заданное.

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

Преобразование и коэрсия

Некоторые схемы поддерживают приведение типов перед проверкой.

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

const CoercedNumber = coerce(number(), string(), value => Number(value))

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

Подобный механизм используется для:

  • обработки данных из HTTP-запросов
  • работы с формами
  • интеграции внешних API

Уточнение правил (refinement)

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

import { refine, string } from 'superstruct'

const NonEmptyString = refine(string(), 'nonEmpty', value => value.length > 0)

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

Refinement позволяет выражать:

  • диапазоны значений
  • регулярные выражения
  • сложные доменные правила

Валидация и обработка ошибок

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

Ошибки содержат:

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

Это позволяет точно локализовать проблему внутри вложенных объектов.

Пример типичного поведения:

  • при ошибке в user.profile.age указывается полный путь
  • при массиве — индекс элемента

Композиция структур

Схемы в Superstruct проектируются как композиционные элементы. Более сложные структуры строятся из простых без дублирования логики.

const Id = number()
const Name = string()

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

Композиция позволяет:

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

Работа с вложенными и динамическими структурами

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

const createSchema = (isAdmin) =>
  object({
    name: string(),
    permissions: isAdmin ? array(string()) : literal(null),
  })

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

Типизация и интеграция с JavaScript

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

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

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

Поведение при проверке данных

Процесс валидации проходит поэтапно:

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

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