Валидация данных в Superstruct строится вокруг базовых структур (string, number, object и т.д.), однако реальная прикладная логика часто требует более строгих ограничений, чем могут выразить стандартные типы. Для этого используется механизм refine, позволяющий накладывать дополнительные условия поверх уже существующей структуры.
Ключевая идея refine заключается в расширении базовой проверки предикатом, который должен подтвердить корректность значения.
Основные свойства refine:
В 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 заключается в том, что он работает после базовой валидации структуры. Это означает:
Такой порядок предотвращает необходимость ручной проверки типов внутри предиката.
Созданные структуры можно комбинировать и переиспользовать.
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 редко используется изолированно. Чаще он является завершающим слоем поверх комбинации структур.
import { array, number, refine } from 'superstruct'
const EvenNumbersArray = refine(
array(number),
'EvenNumbersArray',
(value) => value.every((n) => n % 2 === 0)
)
Refine хорошо подходит для композиции бизнес-логики:
const BaseId = refine(string, 'BaseId', (value) => value.length === 10)
const NumericId = refine(BaseId, 'NumericId', (value) => /^\d+$/.test(value))
Refine используется в случаях, когда:
Без 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 выполняет функцию моста между:
Он позволяет удерживать сложную валидацию в декларативной форме, не переходя к императивным проверкам вне схем.