В библиотеке Superstruct основная идея заключается в построении проверяемых структур данных через композицию примитивов и фабрик структур. Собственные типы формируются как расширения базовых структур, обеспечивая строгую валидацию и повторное использование логики.
Ключевым элементом является возможность описывать доменные сущности в виде отдельных, изолированных структур, которые затем комбинируются в более сложные модели.
Пример базового пользовательского типа:
import { struct } from 'superstruct'
const PositiveNumber = struct({
name: 'PositiveNumber',
validator: (value) => typeof value === 'number' && value > 0
})
Такой подход позволяет формализовать правила, которые не покрываются
стандартными примитивами вроде number или
string.
В современных версиях Superstruct предпочтительным способом создания
собственных типов является define. Он предоставляет более
выразительный интерфейс для описания валидации.
import { define } from 'superstruct'
const PositiveNumber = define('PositiveNumber', (value) => {
return typeof value === 'number' && value > 0
})
Механизм define позволяет создавать именованные
структуры, которые участвуют в системе ошибок и диагностики. Имя
структуры становится частью сообщения об ошибке, что упрощает отладку
сложных схем данных.
Пример более сложного типа:
const EvenNumber = define('EvenNumber', (value) => {
return typeof value === 'number' && value % 2 === 0
})
Пользовательские структуры часто используются для инкапсуляции правил предметной области. Вместо разрозненных проверок создаются переиспользуемые типы.
const UserId = define('UserId', (value) => {
return typeof value === 'string' && value.length === 24
})
const Email = define('Email', (value) => {
return typeof value === 'string' && value.includes('@')
})
Такие определения позволяют выстраивать слой валидации, отделённый от бизнес-логики.
Одним из ключевых механизмов Superstruct является композиция структур. Пользовательские типы могут быть встроены в более сложные схемы.
import { object } from 'superstruct'
const User = object({
id: UserId,
email: Email,
score: PositiveNumber
})
Композиция обеспечивает строгую типизацию вложенных объектов без дублирования логики валидации.
Функция refine используется для добавления
дополнительных ограничений к уже существующим структурам.
import { refine, string } from 'superstruct'
const ShortString = refine(string(), 'ShortString', (value) => {
return value.length <= 10
})
Механизм refine позволяет накладывать дополнительные
условия поверх базовых типов, сохраняя при этом исходную структуру.
Пример каскадных ограничений:
const NonEmptyString = refine(string(), 'NonEmptyString', (value) => {
return value.length > 0
})
const Username = refine(NonEmptyString, 'Username', (value) => {
return /^[a-z0-9_]+$/.test(value)
})
При масштабировании приложения пользовательские типы обычно выносятся в отдельные модули. Это формирует слой схем, независимый от остальной логики.
Пример структуры:
/schemas
user.js
product.js
shared.js
Файл user.js:
import { object } from 'superstruct'
import { UserId, Email } from './shared'
export const User = object({
id: UserId,
email: Email
})
Такой подход обеспечивает повторное использование и централизованное управление схемами.
Пользовательские типы могут быть оформлены как отдельная библиотека для повторного использования в нескольких проектах.
Типичная структура пакета:
/src
index.js
types/
user.js
order.js
utils/
validation.js
Файл экспорта:
export { User } from './types/user'
export { Order } from './types/order'
После публикации пакет используется как зависимость:
import { User } from '@company/schemas'
Сложные системы часто требуют комбинирования нескольких правил в
одном типе. Для этого используются функции intersection и
union.
import { intersection, object, string } from 'superstruct'
const Named = object({
name: string()
})
const Timestamped = object({
createdAt: string()
})
const NamedEntity = intersection([Named, Timestamped])
Такой подход позволяет строить гибкие модели данных без дублирования описаний.
Для уменьшения повторений часто используются фабрики структур:
const createMinLengthString = (min) =>
refine(string(), `MinLength(${min})`, (value) => {
return value.length >= min
})
const Password = createMinLengthString(8)
Фабрики позволяют создавать параметризованные типы, которые масштабируются без увеличения сложности кода.
Каждая структура в Superstruct возвращает детализированные ошибки, содержащие имя типа и путь до некорректного значения.
import { validate } from 'superstruct'
const [error, result] = validate('abc', Email)
Пользовательские типы автоматически участвуют в формировании сообщений:
Expected an Email, received "abc"
Именование структуры напрямую влияет на читаемость ошибок, поэтому корректная регистрация типов становится важной частью архитектуры.
Хотя Superstruct не зависит от TypeScript, пользовательские структуры часто используются вместе с типами TypeScript для синхронизации runtime и compile-time проверки.
import { Infer, object, string } from 'superstruct'
const User = object({
email: string()
})
type UserType = Infer<typeof User>
Пользовательские структуры становятся источником правды для генерации типов.
В крупных системах пользовательские типы разбиваются на уровни:
Пример:
const OrderItem = object({
productId: ProductId,
quantity: PositiveNumber
})
const Order = object({
id: OrderId,
items: array(OrderItem)
})
Такое разделение позволяет контролировать сложность модели и уменьшает связанность компонентов.
Хотя Superstruct не реализует классическое наследование, поведение
типов может быть расширено через композицию и refine.
const BaseId = define('BaseId', (value) => {
return typeof value === 'string'
})
const UUID = refine(BaseId, 'UUID', (value) => {
return /^[0-9a-fA-F-]{36}$/.test(value)
})
Подобная схема имитирует иерархию типов без жёсткой связи между ними.
В проектах с множеством разработчиков пользовательские типы часто стандартизируются:
Это приводит к тому, что Superstruct становится не просто библиотекой валидации, а слоем описания доменной модели приложения.
При изменении структуры данных важно учитывать обратную совместимость. Новые версии типов обычно вводятся параллельно со старыми:
const UserV1 = object({
id: UserId,
email: Email
})
const UserV2 = object({
id: UserId,
email: Email,
name: string()
})
Постепенная миграция позволяет избежать нарушения контрактов между сервисами.
Для больших наборов пользовательских структур используется индексирование:
export const Types = {
User,
Order,
Product
}
Такой подход упрощает динамическое обращение к схемам и их регистрацию в валидаторах верхнего уровня.
При работе с API пользовательские структуры часто используются как контракт между клиентом и сервером.
const CreateUserRequest = object({
email: Email,
password: Password
})
const response = validate(requestBody, CreateUserRequest)
Это обеспечивает единообразную валидацию входящих данных независимо от источника запроса.
В случаях, когда структура зависит от условий, используется
комбинация union и refine.
import { union } from 'superstruct'
const Admin = object({
role: literal('admin'),
permissions: array(string())
})
const Guest = object({
role: literal('guest')
})
const UserRole = union([Admin, Guest])
Такой подход позволяет описывать вариативные структуры без потери строгости проверки.