Валидация данных в Superstruct строится вокруг идеи маленьких, независимых проверок, которые можно комбинировать в более сложные схемы. Когда структура начинает включать преобразования, нормализацию и последовательные ограничения, возникает необходимость в композиции — механизме, который позволяет выстраивать цепочку операций над значением.
Одним из ключевых инструментов становится pipe, позволяющий последовательно применять несколько структур и трансформаций к одному значению, превращая простую проверку в полноценный конвейер обработки данных.
pipe в Superstruct используется для последовательного применения нескольких структур или функций валидации. Каждый шаг получает результат предыдущего, формируя линейный поток обработки данных.
Ключевая идея:
Простейшая модель:
pipe(structA, structB, structC)
Где:
structA — первичная проверка или преобразование;structB — дополнительное ограничение;structC — финальная форма результата.Рассмотрим типичную задачу: строка должна быть очищена от пробелов и приведена к нижнему регистру, а затем проверена на минимальную длину.
import { string, size, pipe, refine } from 'superstruct'
const trim = (value) => value.trim()
const toLower = (value) => value.toLowerCase()
const CleanString = pipe(
string(),
(value) => trim(value),
(value) => toLower(value)
)
В этой конструкции:
string() гарантирует тип;Одно из преимуществ pipe — разделение ответственности между этапами.
Можно явно выделить:
Пример:
import { string, size, pipe, refine } from 'superstruct'
const Trim = (v) => v.trim()
const Lower = (v) => v.toLowerCase()
const Username = pipe(
string(),
Trim,
Lower,
size(3, 20),
refine((value) => /^[a-z0-9_]+$/.test(value))
)
Здесь логика выстраивается как конвейер:
Важное свойство pipe — декларативность. Вместо императивного кода с промежуточными переменными формируется линейная цепочка описаний.
Сравнение подходов:
Императивный стиль:
let value = input.trim()
value = value.toLowerCase()
if (value.length < 3) {
throw new Error('Too short')
}
Декларативный через pipe:
const Struct = pipe(
string(),
(v) => v.trim(),
(v) => v.toLowerCase(),
size(3, 20)
)
Разница проявляется в том, что pipeline описывает не алгоритм, а последовательность преобразований.
Pipe можно рассматривать как строительный блок для более сложных структур. Его можно вкладывать в другие структуры, создавая многоуровневую систему обработки данных.
Пример: объект пользователя с нормализацией полей.
import { object, string, pipe } from 'superstruct'
const Trim = (v) => v.trim()
const Username = pipe(string(), Trim)
const Email = pipe(string(), Trim)
const User = object({
username: Username,
email: Email
})
Здесь каждый компонент остаётся переиспользуемым, а объектная структура лишь собирает их вместе.
Параллельно с pipe часто используется концептуальная модель flow — поток обработки данных, где каждое преобразование рассматривается как часть единого процесса.
Если pipe можно представить как строго линейную цепочку, то flow чаще трактуется шире:
Хотя Superstruct формально ориентирован на pipe как основной механизм, модель flow помогает проектировать сложные схемы валидации.
Flow проявляется там, где данные проходят не только линейную обработку, но и логическую трансформацию состояния.
Пример: обработка формы регистрации.
const normalize = (v) => v.trim().toLowerCase()
const preprocess = (value) => ({
original: value,
normalized: normalize(value)
})
Такой этап можно встроить в pipe:
const Username = pipe(
string(),
preprocess,
(v) => v.normalized,
size(3, 20)
)
Здесь flow выражается в том, что данные сначала расширяются, затем извлекается нужное поле.
Superstruct позволяет сочетать pipe с базовыми конструкциями:
stringnumberobjectarrayunionrefineПример с массивами:
import { array, string, pipe } from 'superstruct'
const Trim = (v) => v.trim()
const Tags = pipe(
array(string()),
(arr) => arr.map(Trim),
(arr) => arr.filter(Boolean)
)
Каждый элемент массива проходит через нормализацию, после чего массив очищается от пустых значений.
Pipe особенно полезен для приведения данных к числовому виду.
import { string, number, pipe } from 'superstruct'
const toNumber = (v) => Number(v)
const ensureFinite = (v) => Number.isFinite(v) ? v : NaN
const Age = pipe(
string(),
toNumber,
ensureFinite,
(v) => (v >= 0 ? v : NaN)
)
Такой подход позволяет обрабатывать данные, поступающие из внешних источников (формы, API), где типы часто не гарантированы.
Каждый этап pipe может прервать выполнение всей цепочки. Это делает поведение предсказуемым: ошибка локализуется в конкретном шаге.
Типичная ситуация:
Пример:
const Struct = pipe(
string(),
(v) => JSON.parse(v),
object({
id: string()
})
)
Если строка не является валидным JSON, цепочка прерывается на этапе парсинга.
Одним из преимуществ pipe является возможность вынесения отдельных шагов в независимые функции.
const trim = (v) => v.trim()
const normalize = (v) => v.toLowerCase()
const removeSpaces = (v) => v.replace(/\s+/g, '_')
const Slug = pipe(
string(),
trim,
normalize,
removeSpaces
)
Такая структура упрощает тестирование и поддержку, поскольку каждый шаг можно проверять отдельно.
При росте системы валидации важно избегать монолитных структур. Pipe решает эту проблему за счёт:
Даже сложные схемы остаются разложенными на небольшие операции, каждая из которых выполняет одну задачу.
Несмотря на гибкость, pipe имеет естественные ограничения:
Эти ограничения компенсируются архитектурным подходом: сложные ветвления выносятся на уровень подготовки данных, а pipe остаётся чистым конвейером преобразований.
В реальных системах pipe часто используется для отделения бизнес-логики от инфраструктурной обработки данных.
Например:
const PreparedEmail = pipe(
string(),
(v) => v.trim(),
(v) => v.toLowerCase(),
refine((v) => v.includes('@'))
)
Таким образом, бизнес-правила становятся частью декларативной цепочки, а не разрозненным кодом в разных местах приложения.