Публикация собственных типов

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

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

Пример базового пользовательского типа:

import { struct } from 'superstruct'

const PositiveNumber = struct({
  name: 'PositiveNumber',
  validator: (value) => typeof value === 'number' && value > 0
})

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


Использование define для пользовательских структур

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

import { define } from 'superstruct'

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

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

Пример более сложного типа:

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

Инкапсуляция доменной логики

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

const UserId = define('UserId', (value) => {
  return typeof value === 'string' && value.length === 24
})

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

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


Композиция пользовательских типов

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

import { object } from 'superstruct'

const User = object({
  id: UserId,
  email: Email,
  score: PositiveNumber
})

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


Расширение существующих структур через refine

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

import { refine, string } from 'superstruct'

const ShortString = refine(string(), 'ShortString', (value) => {
  return value.length <= 10
})

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

Пример каскадных ограничений:

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

const Username = refine(NonEmptyString, 'Username', (value) => {
  return /^[a-z0-9_]+$/.test(value)
})

Организация набора типов в модульной структуре

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

Пример структуры:

/schemas
  user.js
  product.js
  shared.js

Файл user.js:

import { object } from 'superstruct'
import { UserId, Email } from './shared'

export const User = object({
  id: UserId,
  email: Email
})

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


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

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

Типичная структура пакета:

/src
  index.js
  types/
    user.js
    order.js
  utils/
    validation.js

Файл экспорта:

export { User } from './types/user'
export { Order } from './types/order'

После публикации пакет используется как зависимость:

import { User } from '@company/schemas'

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

Сложные системы часто требуют комбинирования нескольких правил в одном типе. Для этого используются функции intersection и union.

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

const Named = object({
  name: string()
})

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

const NamedEntity = intersection([Named, Timestamped])

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


Переиспользуемые фабрики типов

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

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

const Password = createMinLengthString(8)

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


Работа с ошибками пользовательских типов

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

import { validate } from 'superstruct'

const [error, result] = validate('abc', Email)

Пользовательские типы автоматически участвуют в формировании сообщений:

Expected an Email, received "abc"

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


Интеграция с TypeScript-экосистемой

Хотя Superstruct не зависит от TypeScript, пользовательские структуры часто используются вместе с типами TypeScript для синхронизации runtime и compile-time проверки.

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

const User = object({
  email: string()
})

type UserType = Infer<typeof User>

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


Декомпозиция сложных доменных моделей

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

  • примитивные (string, number с ограничениями)
  • доменные (Email, UserId)
  • агрегаты (User, Order)
  • композиции (UserWithOrders)

Пример:

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

const Order = object({
  id: OrderId,
  items: array(OrderItem)
})

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


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

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

const BaseId = define('BaseId', (value) => {
  return typeof value === 'string'
})

const UUID = refine(BaseId, 'UUID', (value) => {
  return /^[0-9a-fA-F-]{36}$/.test(value)
})

Подобная схема имитирует иерархию типов без жёсткой связи между ними.


Стандартизация пользовательских схем

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

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

Это приводит к тому, что Superstruct становится не просто библиотекой валидации, а слоем описания доменной модели приложения.


Версионирование пользовательских типов

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

const UserV1 = object({
  id: UserId,
  email: Email
})

const UserV2 = object({
  id: UserId,
  email: Email,
  name: string()
})

Постепенная миграция позволяет избежать нарушения контрактов между сервисами.


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

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

export const Types = {
  User,
  Order,
  Product
}

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


Интеграция пользовательских типов в API-слой

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

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

const response = validate(requestBody, CreateUserRequest)

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


Комплексные пользовательские типы с условной логикой

В случаях, когда структура зависит от условий, используется комбинация union и refine.

import { union } from 'superstruct'

const Admin = object({
  role: literal('admin'),
  permissions: array(string())
})

const Guest = object({
  role: literal('guest')
})

const UserRole = union([Admin, Guest])

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