Создание собственных структур

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


Базовый принцип пользовательских структур

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

Создание собственной структуры начинается с функции struct:

import { struct } fr om 'superstruct'

Функция позволяет определить новую структуру на основе уже существующих правил или полностью вручную.


Простейшая пользовательская структура

Самый базовый вариант — обёртка над функцией проверки:

import { struct } from 'superstruct'

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

Здесь создаётся структура, которая принимает только положительные числа.

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

PositiveNumber(10) // 10
PositiveNumber(-5) // ошибка валидации

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


Использование готовых структур внутри кастомных

Пользовательские структуры часто строятся поверх встроенных примитивов:

import { struct, number } from 'superstruct'

const PositiveNumber = struct((value) => {
  return number(value) && value > 0
})

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


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

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

import { number, refine } from 'superstruct'

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

Здесь:

  • number — базовая структура
  • 'PositiveNumber' — имя новой структуры
  • функция — дополнительное ограничение

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


Комбинирование структур

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

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

const Username = refine(string, 'Username', (value) => {
  return value.length >= 3 && value.length <= 20
})

const User = object({
  name: Username,
  email: string(),
})

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


Структуры с преобразованием данных

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

import { struct } from 'superstruct'

const TrimmedString = struct((value) => {
  if (typeof value !== 'string') return false
  return value.trim()
})

Такая структура возвращает уже обработанное значение.

Более корректный вариант с явной логикой проверки и преобразования:

import { struct } from 'superstruct'

const TrimmedString = struct((value) => {
  if (typeof value !== 'string') return false

  const trimmed = value.trim()
  return trimmed.length > 0 ? trimmed : false
})

Фабрики структур

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

import { refine, string } from 'superstruct'

const MinLengthString = (min) =>
  refine(string, `MinLengthString(${min})`, (value) => {
    return value.length >= min
  })

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

const Password = MinLengthString(8)

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


Параметризованные структуры

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

const RangeNumber = (min, max) =>
  refine(number, `RangeNumber(${min}, ${max})`, (value) => {
    return value >= min && value <= max
  })

Пример использования:

const Age = RangeNumber(0, 120)
const Rating = RangeNumber(1, 5)

Одна логика — разные ограничения.


Композиция сложных структур

Пользовательские структуры можно комбинировать в глубоко вложенные схемы:

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

const Product = object({
  title: string(),
  price: number(),
})

const Catalog = object({
  items: array(Product),
})

При необходимости можно подменять отдельные элементы на кастомные структуры:

const Price = refine(number, 'Price', (value) => value >= 0)

const Product = object({
  title: string(),
  price: Price,
})

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

Пользовательские структуры в Superstruct удобно выносить в отдельные модули:

// structures/username.js
import { refine, string } from 'superstruct'

export const Username = refine(string, 'Username', (value) => {
  return value.length >= 3 && value.length <= 20
})

И использовать в разных частях приложения:

import { Username } from './structures/username'

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


Структуры для доменной модели

Создание пользовательских структур особенно полезно при моделировании предметной области:

const Email = refine(string, 'Email', (value) => {
  return value.includes('@') && value.includes('.')
})

const UserRole = refine(string, 'UserRole', (value) => {
  return ['admin', 'user', 'guest'].includes(value)
})

const User = object({
  email: Email,
  role: UserRole,
})

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


Вложенные кастомные правила

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

const PositiveInteger = refine(number, 'PositiveInteger', (v) => v > 0 && Number.isInteger(v))

const Pagination = object({
  page: PositiveInteger,
  lim it: PositiveInteger,
})

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


Использование структур как строительных блоков

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

  • базовые типы (Email, Id, DateString)
  • бизнес-ограничения (PositivePrice, AgeLimit)
  • агрегаты (User, Order, Product)

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