Refine

Назначение механизма refine

Валидация данных в Superstruct строится вокруг базовых структур (string, number, object и т.д.), однако реальная прикладная логика часто требует более строгих ограничений, чем могут выразить стандартные типы. Для этого используется механизм refine, позволяющий накладывать дополнительные условия поверх уже существующей структуры.

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

Основные свойства refine:

  • добавляет пользовательскую логику валидации;
  • работает поверх уже определённого struct;
  • позволяет возвращать кастомные ошибки;
  • не изменяет исходную структуру, а расширяет её поведение.

Базовый синтаксис

В Superstruct refine применяется как функция-обёртка:

import { string, refine } from 'superstruct'

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

Здесь:

  • string — базовая структура;
  • 'NonEmptyString' — имя нового типа (используется в сообщениях об ошибках);
  • функция-предикат определяет условие валидности.

Если предикат возвращает false, значение считается невалидным.


Принцип работы предиката

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

(value) => boolean

Поведение:

  • true — значение корректно;
  • false — значение не проходит проверку.

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


Создание ограничений поверх примитивов

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

import { number, refine } from 'superstruct'

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

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


Ограничение по диапазону

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

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


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

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

Использование сложных условий

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

const Password = refine(string, 'Password', (value) => {
  const hasNumber = /\d/.test(value)
  const hasLetter = /[a-zA-Z]/.test(value)
  const longEnough = value.length >= 8

  return hasNumber && hasLetter && longEnough
})

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


Работа с объектами

Refine можно применять не только к примитивам, но и к объектам, прошедшим базовую проверку struct.

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

const User = refine(
  object({
    name: string,
    age: number,
  }),
  'User',
  (value) => {
    return value.age >= 18
  }
)

Здесь сначала проверяется структура объекта, затем накладывается дополнительное ограничение на значение age.


Постструктурная валидация

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

  1. Сначала проверяются типы полей;
  2. Затем выполняется пользовательская логика;
  3. Только после этого значение считается валидным.

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


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

Созданные структуры можно комбинировать и переиспользовать.

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

const PositiveEvenNumber = refine(EvenNumber, 'PositiveEvenNumber', (value) => {
  return value > 0
})

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


Поведение ошибок

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

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

Если передано значение "hello world", ошибка будет связана с нарушением ограничения refine, а не базового типа string.


Кастомизация логики через параметры замыкания

Refine позволяет использовать внешние параметры через замыкания.

const minLength = 5

const MinLengthString = refine(string, 'MinLengthString', (value) => {
  return value.length >= minLength
})

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


Ограничения подхода refine

Несмотря на гибкость, refine имеет архитектурные ограничения:

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

Комбинирование refine с базовыми struct

На практике refine редко используется изолированно. Чаще он является завершающим слоем поверх комбинации структур.

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

const EvenNumbersArray = refine(
  array(number),
  'EvenNumbersArray',
  (value) => value.every((n) => n % 2 === 0)
)

Композиция правил валидации

Refine хорошо подходит для композиции бизнес-логики:

  • базовый тип → проверка структуры;
  • refine → проверка правил;
  • дополнительные refine → расширение ограничений.
const BaseId = refine(string, 'BaseId', (value) => value.length === 10)

const NumericId = refine(BaseId, 'NumericId', (value) => /^\d+$/.test(value))

Типичные сценарии применения

Refine используется в случаях, когда:

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

Сравнение с базовой валидацией struct

Без refine:

string
number
object({...})

С refine:

refinedStruct = refine(baseStruct, name, predicate)

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


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

Паттерн «доменное ограничение»

Используется для выражения правил предметной области.

const OrderQuantity = refine(number, 'OrderQuantity', (v) => v > 0 && v < 1000)

Паттерн «валидация форм»

const Login = refine(string, 'Login', (v) => v.includes('@'))

Паттерн «композиция ограничений»

const BaseString = string

const TrimmedString = refine(BaseString, 'TrimmedString', (v) => v === v.trim())

const SafeString = refine(TrimmedString, 'SafeString', (v) => !v.includes('<script>'))

Роль refine в архитектуре валидации

Refine выполняет функцию моста между:

  • типовой системой структур;
  • прикладной бизнес-логикой;
  • контекстными ограничениями данных.

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