Валидационная логика в Superstruct строится вокруг композиции небольших примитивов и их объединения в более сложные структуры. Именно в этом подходе проявляется ключевая идея переиспользования: вместо дублирования проверок создаются универсальные строительные блоки, которые затем комбинируются в разных контекстах.
В основе повторного использования лежат примитивные структуры:
string, number, boolean,
array, object. Они выступают не просто как
типы, а как функции, которые можно многократно включать в более сложные
схемы.
import { string, number, object } from 'superstruct'
const Name = string()
const Age = number()
const User = object({
name: Name,
age: Age
})
Такой подход уже формирует первый уровень переиспользования: базовые правила не встраиваются внутрь объектов напрямую, а выделяются в отдельные сущности.
Часто логика валидации выходит за рамки простых типов. Например, имя
пользователя должно быть непустым, а возраст — положительным числом.
Вместо того чтобы повторять эти условия в каждом объекте, используется
refine.
import { string, number, refine } from 'superstruct'
const NonEmptyString = refine(string(), 'NonEmptyString', (value) => {
return value.trim().length > 0
})
const PositiveNumber = refine(number(), 'PositiveNumber', (value) => {
return value > 0
})
Теперь эти структуры становятся универсальными ограничителями, которые можно использовать в любых схемах без повторного описания условий.
Superstruct позволяет объединять структуры в новые типы без потери читаемости и гибкости. Это достигается через вложенные объекты и переиспользование ранее определённых структур.
import { object } from 'superstruct'
const Address = object({
city: string(),
zip: string()
})
const UserProfile = object({
name: NonEmptyString,
age: PositiveNumber,
address: Address
})
Здесь Address становится самостоятельным модулем
валидации, который может использоваться в других частях приложения без
изменений.
При необходимости создания однотипных схем с параметрами используется подход фабрик. Это позволяет абстрагировать повторяющиеся паттерны.
import { string, refine } from 'superstruct'
const MinLengthString = (min) =>
refine(string(), `MinLength(${min})`, (value) => {
return value.length >= min
})
const Username = MinLengthString(3)
const Password = MinLengthString(8)
Фабрики позволяют переносить логику валидации на уровень конфигурации, избегая дублирования одинаковых проверок.
Когда необходимо объединить несколько правил в одно, используется
intersect. Это позволяет комбинировать уже существующие
структуры без их переписывания.
import { intersect, string, pattern } from 'superstruct'
const AlphaString = pattern(string(), /^[A-Za-z]+$/)
const UppercaseString = pattern(string(), /^[A-Z]+$/)
const StrictCode = intersect([AlphaString, UppercaseString])
Такой подход особенно полезен, когда разные команды или модули определяют собственные ограничения, которые затем объединяются.
Часто логика валидации зависит не от типа данных, а от бизнес-правил. В таких случаях создаются универсальные валидаторы, принимающие параметры.
import { refine } from 'superstruct'
const InRange = (min, max) =>
refine(number(), `InRange(${min},${max})`, (value) => {
return value >= min && value <= max
})
const Rating = InRange(1, 5)
const Percentage = InRange(0, 100)
Такой подход позволяет стандартизировать диапазонные проверки и использовать их в разных частях системы.
В крупных проектах формируется слой повторно используемых доменных типов. Они не привязаны к конкретным объектам API, а описывают универсальные сущности предметной области.
const Email = refine(string(), 'Email', (value) => {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
})
const ID = refine(string(), 'ID', (value) => {
return value.length === 24
})
Такие структуры становятся фундаментом всей системы, уменьшая количество дублирующейся логики валидации.
Повторное использование логики часто требует расширения существующих структур без их изменения. Для этого создаются функции-обёртки.
const withTrim = (struct) =>
refine(struct, 'withTrim', (value) => {
return typeof value === 'string'
})
Хотя пример упрощён, идея заключается в том, что структура не копируется, а модифицируется через слой абстракции.
Объектные структуры особенно выигрывают от наследования логики через композицию.
const Timestamps = object({
createdAt: string(),
updatedAt: string()
})
const BaseEntity = object({
id: ID,
...Timestamps
})
Такой подход позволяет формировать единый стандарт для всех сущностей приложения, минимизируя расхождения в моделях данных.
Для повышения читаемости и повторного использования часто создаются псевдонимы для сложных структур.
const EmailString = Email
const UserIdentifier = ID
Это особенно полезно в больших кодовых базах, где одна и та же структура используется в десятках мест, но должна оставаться семантически различимой.
Одним из ключевых аспектов переиспользования является вынесение всех правил валидации в отдельные модули. Это обеспечивает единый источник истины для всей системы.
// validators.js
export const RequiredString = refine(string(), 'RequiredString', v => v.length > 0)
export const PositiveInt = refine(number(), 'PositiveInt', v => Number.isInteger(v) && v > 0)
Далее эти структуры импортируются и используются в любых схемах без повторного определения логики.
Иногда требуется использовать только часть уже существующей схемы. В таких случаях логика переиспользуется через выделение подструктур.
const Credentials = object({
email: Email,
password: Password
})
const Login = Credentials
const Register = object({
...Credentials,
name: NonEmptyString
})
Разделение на логические блоки позволяет комбинировать их в различных сценариях без дублирования.
Функции-предикаты, используемые внутри refine, также
становятся инструментом повторного использования. Их можно выносить
отдельно и комбинировать.
const isNonEmpty = (value) => value.trim().length > 0
const isValidZip = (value) => /^[0-9]{5}$/.test(value)
const ZipCode = refine(string(), 'ZipCode', isValidZip)
Предикаты позволяют отделить чистую бизнес-логику от структуры данных.
При росте приложения структуры начинают формировать сложные графы зависимостей. Переиспользование становится способом управления этими связями.
const OrderItem = object({
productId: ID,
quantity: PositiveNumber
})
const Order = object({
id: ID,
items: array(OrderItem),
customerEmail: Email
})
Каждый элемент здесь существует независимо и может быть использован в других моделях, например в корзине или истории заказов.
Повторное использование логики часто касается не только отдельных значений, но и коллекций.
import { array } from 'superstruct'
const EmailList = array(Email)
const IdList = array(ID)
Такой подход позволяет централизовать правила для коллекций, сохраняя единообразие обработки данных.
Когда определённый набор правил встречается многократно, он выделяется в самостоятельную абстракцию.
const AuditedEntity = object({
id: ID,
createdAt: string(),
updatedAt: string()
})
Эта структура становится базовым шаблоном для множества сущностей, снижая вероятность расхождений между моделями данных.
Наиболее масштабируемый подход к переиспользованию заключается в сборке структур из независимых модулей.
const Profile = object({
user: BaseEntity,
credentials: Credentials,
preferences: object({
theme: string(),
language: string()
})
})
Каждая часть может эволюционировать независимо, не затрагивая остальные компоненты системы.