Библиотека Superstruct строится вокруг идеи декларативного описания данных через примитивы и их композиции. На уровне прикладной разработки ключевым этапом становится формирование «финальной формы» структуры — схемы, которая одновременно описывает:
Финальная структура не является просто набором проверок. Это завершённая модель данных, пригодная для интеграции в API-слой, форму ввода, слой доменной логики или контракт между сервисами.
Любая структура в Superstruct начинается с примитивов:
import { string, number, boolean, object } from 'superstruct'
Однако на практике примитивы редко используются изолированно. Финальная форма строится через композицию:
const User = object({
id: number(),
name: string(),
isActive: boolean()
})
Такой вариант уже представляет минимально завершённую модель. Но в реальных системах этого недостаточно: требуется контроль преобразований, обработка отсутствующих полей, нормализация входных данных.
Одной из ключевых особенностей финальной формы становится приведение типов.
Типичный сценарий — данные приходят из HTTP-запроса, где всё представлено строками:
const RawUser = object({
id: string(),
age: string()
})
Финальная форма требует приведения:
import { coerce, number, string, object } from 'superstruct'
const User = coerce(
object({
id: number(),
age: number()
}),
object({
id: string(),
age: string()
}),
(value) => ({
id: Number(value.id),
age: Number(value.age)
})
)
Здесь формируется важный принцип: финальная структура всегда отделяет входной формат от доменного.
Финальная форма часто включает автоматическую нормализацию:
Пример нормализации строкового поля:
import { string, refine } from 'superstruct'
const TrimmedString = refine(string(), 'TrimmedString', (value) => {
return value.trim().length > 0
})
Более функциональный вариант — встроенное преобразование:
const NormalizedString = coerce(
string(),
string(),
(value) => value.trim().toLowerCase()
)
В финальной форме подобные трансформации перестают быть вспомогательными и становятся частью контракта данных.
Финальная структура обязана чётко описывать поведение отсутствующих значений.
import { optional, nullable, string } from 'superstruct'
const Schema = object({
nickname: optional(string()),
middleName: nullable(string())
})
Разница:
В сложных моделях эти состояния комбинируются:
const Profile = object({
bio: optional(nullable(string()))
})
Такая запись задаёт трёхсостояние:
Финальная форма почти всегда состоит из вложенных структур:
const Address = object({
city: string(),
zip: string()
})
const User = object({
name: string(),
address: Address
})
Однако реальная сложность возникает при переиспользовании и расширении схем.
import { assign } from 'superstruct'
const BaseUser = object({
id: number(),
name: string()
})
const AdminUser = assign(
BaseUser,
object({
permissions: string()
})
)
assign формирует новую структуру без изменения базовой. Это ключевой механизм построения иерархий.
Финальная форма часто включает альтернативные варианты данных:
import { union } from 'superstruct'
const Response = union([
object({
status: string(),
data: string()
}),
object({
status: string(),
error: string()
})
])
Union становится критическим элементом API-контрактов, где результат может быть либо успешным, либо ошибочным.
Финальная форма редко ограничивается стандартными типами. Часто требуется доменная логика:
import { define } from 'superstruct'
const PositiveNumber = define('PositiveNumber', (value) => {
return typeof value === 'number' && value > 0
})
Использование:
const Product = object({
price: PositiveNumber,
quantity: PositiveNumber
})
Такой подход переносит бизнес-правила внутрь структуры, делая её самодостаточной.
В сложных системах структура включает много уровней вложенности:
const Comment = object({
text: string(),
author: object({
id: number(),
name: string()
})
})
Финальная форма требует не только проверки верхнего уровня, но и полной рекурсивной валидации.
Валидация в Superstruct возвращает структурированные ошибки:
import { validate } from 'superstruct'
const [error, result] = validate(data, User)
Ошибка содержит:
Финальная форма часто требует нормализации ошибок в прикладной слой:
const formatError = (error) => ({
field: error.path.join('.'),
message: error.message
})
Финальная схема может строиться на основе условий:
const createSchema = (isAdmin) =>
object({
id: number(),
role: string(),
permissions: isAdmin ? string() : optional(string())
})
Такой подход позволяет адаптировать модель данных под контекст выполнения.
В архитектуре крупных приложений схема часто собирается по слоям:
Пример:
const Id = number()
const Name = string()
const Entity = object({
id: Id,
name: Name
})
const Timestamped = object({
createdAt: string(),
updatedAt: string()
})
const User = assign(Entity, Timestamped)
Финальная структура становится результатом композиции независимых модулей.
Финальная схема обычно используется на границе системы:
Пример API-валидации:
const handler = (req, res) => {
const [error, body] = validate(req.body, User)
if (error) {
res.status(400).json(error.failures())
return
}
// body гарантированно валиден
}
Финальная форма выполняет функцию контракта:
В системах с множеством сервисов именно схема становится точкой согласования между компонентами.
Критически важный аспект финальной формы — отделение внешнего и внутреннего представления:
const ApiUser = object({
id: string(),
full_name: string()
})
const DomainUser = coerce(
object({
id: number(),
name: string()
}),
ApiUser,
(value) => ({
id: Number(value.id),
name: value.full_name
})
)
Такое разделение позволяет менять внешний контракт без влияния на доменную логику.
При росте проекта структура превращается в систему:
Superstruct остаётся стабильным ядром, вокруг которого строится контрактная модель данных, где финальная форма выступает точкой интеграции всех слоёв.