Final Form

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

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

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


Базовая модель финальной структуры

Любая структура в Superstruct начинается с примитивов:

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

Однако на практике примитивы редко используются изолированно. Финальная форма строится через композицию:

const User = object({
  id: number(),
  name: string(),
  isActive: boolean()
})

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


Преобразование входных данных (coercion)

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

Типичный сценарий — данные приходят из HTTP-запроса, где всё представлено строками:

const RawUser = object({
  id: string(),
  age: string()
})

Финальная форма требует приведения:

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

const User = coerce(
  object({
    id: number(),
    age: number()
  }),
  object({
    id: string(),
    age: string()
  }),
  (value) => ({
    id: Number(value.id),
    age: Number(value.age)
  })
)

Здесь формируется важный принцип: финальная структура всегда отделяет входной формат от доменного.


Нормализация данных как часть схемы

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

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

Пример нормализации строкового поля:

import { string, refine } from 'superstruct'

const TrimmedString = refine(string(), 'TrimmedString', (value) => {
  return value.trim().length > 0
})

Более функциональный вариант — встроенное преобразование:

const NormalizedString = coerce(
  string(),
  string(),
  (value) => value.trim().toLowerCase()
)

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


Работа с optional и nullable полями

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

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

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

Разница:

  • optional — поле может отсутствовать полностью
  • nullable — поле присутствует, но может быть null

В сложных моделях эти состояния комбинируются:

const Profile = object({
  bio: optional(nullable(string()))
})

Такая запись задаёт трёхсостояние:

  • нет поля,
  • поле есть и null,
  • поле есть и строка.

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

Финальная форма почти всегда состоит из вложенных структур:

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

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

Однако реальная сложность возникает при переиспользовании и расширении схем.

Расширение структур

import { assign } from 'superstruct'

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

const AdminUser = assign(
  BaseUser,
  object({
    permissions: string()
  })
)

assign формирует новую структуру без изменения базовой. Это ключевой механизм построения иерархий.


Условные структуры и union-типы

Финальная форма часто включает альтернативные варианты данных:

import { union } from 'superstruct'

const Response = union([
  object({
    status: string(),
    data: string()
  }),
  object({
    status: string(),
    error: string()
  })
])

Union становится критическим элементом API-контрактов, где результат может быть либо успешным, либо ошибочным.


Пользовательские валидаторы

Финальная форма редко ограничивается стандартными типами. Часто требуется доменная логика:

import { define } from 'superstruct'

const PositiveNumber = define('PositiveNumber', (value) => {
  return typeof value === 'number' && value > 0
})

Использование:

const Product = object({
  price: PositiveNumber,
  quantity: PositiveNumber
})

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


Глубокая валидация вложенных объектов

В сложных системах структура включает много уровней вложенности:

const Comment = object({
  text: string(),
  author: object({
    id: number(),
    name: string()
  })
})

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


Обработка ошибок как часть финальной формы

Валидация в Superstruct возвращает структурированные ошибки:

import { validate } from 'superstruct'

const [error, result] = validate(data, User)

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

  • путь к полю,
  • ожидаемый тип,
  • фактическое значение.

Финальная форма часто требует нормализации ошибок в прикладной слой:

const formatError = (error) => ({
  field: error.path.join('.'),
  message: error.message
})

Динамические структуры

Финальная схема может строиться на основе условий:

const createSchema = (isAdmin) =>
  object({
    id: number(),
    role: string(),
    permissions: isAdmin ? string() : optional(string())
  })

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


Паттерн “сборка финальной структуры”

В архитектуре крупных приложений схема часто собирается по слоям:

  1. базовые примитивы,
  2. доменные типы,
  3. переиспользуемые блоки,
  4. прикладные расширения,
  5. финальная композиция.

Пример:

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

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

const Timestamped = object({
  createdAt: string(),
  updatedAt: string()
})

const User = assign(Entity, Timestamped)

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


Интеграция финальной формы в прикладные слои

Финальная схема обычно используется на границе системы:

  • HTTP API,
  • формы ввода,
  • очереди сообщений,
  • интеграции с внешними сервисами.

Пример API-валидации:

const handler = (req, res) => {
  const [error, body] = validate(req.body, User)

  if (error) {
    res.status(400).json(error.failures())
    return
  }

  // body гарантированно валиден
}

Стабилизация контрактов данных

Финальная форма выполняет функцию контракта:

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

В системах с множеством сервисов именно схема становится точкой согласования между компонентами.


Разделение доменной и транспортной модели

Критически важный аспект финальной формы — отделение внешнего и внутреннего представления:

const ApiUser = object({
  id: string(),
  full_name: string()
})

const DomainUser = coerce(
  object({
    id: number(),
    name: string()
  }),
  ApiUser,
  (value) => ({
    id: Number(value.id),
    name: value.full_name
  })
)

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


Масштабирование финальных схем

При росте проекта структура превращается в систему:

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

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