Расширение базовых типов

В Superstruct примитивные структуры — это строительные блоки, на которых формируется любая схема валидации. К ним относятся string, number, boolean, array, object, а также специализированные виды вроде literal, enums, record. Расширение базовых типов в контексте библиотеки означает не изменение самих примитивов, а создание новых структур поверх них через композицию, уточнение и трансформацию.

Ключевая идея заключается в том, что любой более сложный тип всегда можно выразить через комбинацию базовых структур и функций модификации: refine, coerce, defaulted, optional, union, intersection.


Уточнение типов через refine

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

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

import { number, refine } from 'superstruct'

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

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

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

  • ограничения диапазонов
  • проверки форматов
  • бизнес-логики поверх примитивов

Пример строкового расширения:

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

Создание новых типов через define

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

import { define } from 'superstruct'

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

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

Типичные сценарии:

  • проверка сложных инвариантов
  • валидация внешних форматов (например, хэши, UUID-подобные строки)
  • комбинированные условия

Комбинирование типов через intersection

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

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

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

const Aged = object({
  age: number,
})

const Person = intersect([Named, Aged])

Результатом становится структура, которая объединяет требования обеих схем.

Особенности:

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

Объединение вариантов через union

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

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

const StringOrNumber = union([string, number])

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

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

Модификация поведения через coerce

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

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

const NumberFromString = coerce(number, string, (value) => {
  return Number(value)
})

Это фактически расширение базового типа number возможностью принимать строковое представление.

Типовые сценарии:

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

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

Расширение структуры часто включает автоматическое заполнение отсутствующих полей.

import { defaulted, number } from 'superstruct'

const WithDefault = defaulted(number, 10)

В объектных структурах:

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

const Config = object({
  mode: defaulted(string, 'dev'),
})

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


Расширение объектов через partial и optional

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

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

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

Или через частичное преобразование:

import { partial } from 'superstruct'

const PartialUser = partial(User)

Эти механизмы позволяют:

  • эволюционировать API без ломки совместимости
  • создавать версии схем
  • работать с неполными данными

Композиция массивов и расширение элементов

Базовый тип массива может быть расширен за счёт сложных структур элементов.

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

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

Расширение здесь происходит на уровне элемента массива, а не контейнера.

Дополнительные варианты:

  • массив union-типов
  • массив уточнённых структур
  • массив с coerce-преобразованием элементов

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

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

const Id = refine(string, 'Id', (v) => v.length === 24)

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

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

  • централизовать бизнес-правила
  • уменьшить дублирование логики
  • обеспечить согласованность типов

Расширение через record и динамические ключи

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

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

const Scores = record(string, number)

Это фактически расширение примитивного словаря до типизированной структуры ключ-значение.

Применение:

  • конфигурационные системы
  • словари локализации
  • динамические наборы данных

Ограничение строк как расширение базового string

Базовый string часто расширяется через дополнительные проверки:

  • длина
  • шаблон
  • допустимые символы

Пример через refine:

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

Такое расширение формирует доменные типы поверх примитивов.


Числовые ограничения как расширение number

const Percentage = refine(number, 'Percentage', (value) => {
  return value >= 0 && value <= 100
})

Также возможны комбинированные ограничения:

  • целые числа
  • кратность
  • диапазоны
const EvenPositive = refine(number, 'EvenPositive', (v) => {
  return v > 0 && v % 2 === 0
})

Слои расширения и композиционные цепочки

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

const Base = number

const Positive = refine(Base, 'Positive', v => v > 0)

const EvenPositive = refine(Positive, 'EvenPositive', v => v % 2 === 0)

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


Динамическое расширение через фабрики структур

Иногда требуется генерировать структуры на лету:

const createMinMaxNumber = (min, max) =>
  refine(number, 'MinMaxNumber', (v) => v >= min && v <= max)

Это расширяет базовый тип number параметризованной логикой.

Используется для:

  • конфигураций
  • генерации схем API
  • адаптивной валидации

Ошибки и расширенные описания типов

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

refine(number, 'Age', (v) => v >= 0)

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


Итоговая модель расширения

Расширение базовых типов в Superstruct формируется как комбинация нескольких принципов:

  • уточнение через refine
  • полное определение через define
  • композиция через object, array, intersect
  • альтернативность через union
  • трансформация через coerce
  • параметризация через фабрики
  • ослабление через optional и partial

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