Переиспользование логики

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

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

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

const Name = string()
const Age = number()

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

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

Выделение доменных ограничений

Часто логика валидации выходит за рамки простых типов. Например, имя пользователя должно быть непустым, а возраст — положительным числом. Вместо того чтобы повторять эти условия в каждом объекте, используется refine.

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

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

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

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

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

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

import { object } from 'superstruct'

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

const UserProfile = object({
  name: NonEmptyString,
  age: PositiveNumber,
  address: Address
})

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

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

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

import { string, refine } from 'superstruct'

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

const Username = MinLengthString(3)
const Password = MinLengthString(8)

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

Переиспользование через пересечение структур

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

import { intersect, string, pattern } from 'superstruct'

const AlphaString = pattern(string(), /^[A-Za-z]+$/)
const UppercaseString = pattern(string(), /^[A-Z]+$/)

const StrictCode = intersect([AlphaString, UppercaseString])

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

Обобщённые валидаторы

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

import { refine } from 'superstruct'

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

const Rating = InRange(1, 5)
const Percentage = InRange(0, 100)

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

Слой доменных примитивов

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

const Email = refine(string(), 'Email', (value) => {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
})

const ID = refine(string(), 'ID', (value) => {
  return value.length === 24
})

Такие структуры становятся фундаментом всей системы, уменьшая количество дублирующейся логики валидации.

Расширение структур через композиционные функции

Повторное использование логики часто требует расширения существующих структур без их изменения. Для этого создаются функции-обёртки.

const withTrim = (struct) =>
  refine(struct, 'withTrim', (value) => {
    return typeof value === 'string'
  })

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

Переиспользование объектов через базовые схемы

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

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

const BaseEntity = object({
  id: ID,
  ...Timestamps
})

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

Унификация через алиасы структур

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

const EmailString = Email
const UserIdentifier = ID

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

Централизация правил валидации

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

// validators.js
export const RequiredString = refine(string(), 'RequiredString', v => v.length > 0)
export const PositiveInt = refine(number(), 'PositiveInt', v => Number.isInteger(v) && v > 0)

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

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

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

const Credentials = object({
  email: Email,
  password: Password
})

const Login = Credentials
const Register = object({
  ...Credentials,
  name: NonEmptyString
})

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

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

Функции-предикаты, используемые внутри refine, также становятся инструментом повторного использования. Их можно выносить отдельно и комбинировать.

const isNonEmpty = (value) => value.trim().length > 0
const isValidZip = (value) => /^[0-9]{5}$/.test(value)

const ZipCode = refine(string(), 'ZipCode', isValidZip)

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

Составные доменные модели

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

const OrderItem = object({
  productId: ID,
  quantity: PositiveNumber
})

const Order = object({
  id: ID,
  items: array(OrderItem),
  customerEmail: Email
})

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

Абстракции над массивами и коллекциями

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

import { array } from 'superstruct'

const EmailList = array(Email)
const IdList = array(ID)

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

Инкапсуляция повторяющихся паттернов

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

const AuditedEntity = object({
  id: ID,
  createdAt: string(),
  updatedAt: string()
})

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

Сборка сложных структур из независимых частей

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

const Profile = object({
  user: BaseEntity,
  credentials: Credentials,
  preferences: object({
    theme: string(),
    language: string()
  })
})

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