Валидация объектов в Superstruct строится вокруг строгого описания
структуры данных. Базовый примитив object требует, чтобы
входные данные полностью соответствовали заданной схеме: каждое поле
должно присутствовать, если оно не помечено как опциональное через
отдельные механизмы. Однако в реальных приложениях данные часто приходят
частично: формы сохраняются по шагам, API возвращает неполные
обновления, состояние синхронизируется инкрементально. Для таких случаев
применяется механизм частичных объектов.
Partial-объекты в Superstruct позволяют трансформировать строгую схему объекта в её «ослабленную» версию, где каждое поле становится необязательным, но при этом сохраняет свои правила валидации при наличии значения.
Основная идея partial заключается не в изменении типов полей, а в изменении их обязательности. Если исходная структура требует строку, число или вложенный объект, partial-версия допускает отсутствие этого поля, но не допускает некорректное значение, если поле всё же передано.
import { object, string, number, partial } from 'superstruct'
const User = object({
name: string(),
age: number(),
})
Эта структура требует оба поля. Применение partial:
const PartialUser = partial(User)
Теперь допустимы следующие данные:
PartialUser.create({})
PartialUser.create({ name: 'Alex' })
PartialUser.create({ age: 30 })
PartialUser.create({ name: 'Alex', age: 30 })
Но при этом сохраняется строгая проверка типов:
PartialUser.create({ name: 123 }) // ошибка
Поле name остаётся строкой, просто перестаёт быть
обязательным.
Partial не отключает проверки структурных правил. Он изменяет только требование наличия ключа.
При передаче значения происходит полный цикл валидации:
Это поведение важно, поскольку позволяет использовать partial-структуры как слой поверх существующих схем без их дублирования.
Superstruct поддерживает два подхода к необязательным полям:
optional(struct) — делает конкретное поле
необязательнымpartial(object) — делает все поля объекта
необязательнымиПример с optional:
import { object, string, number, optional } from 'superstruct'
const User = object({
name: string(),
age: optional(number()),
})
Здесь только age может отсутствовать.
Partial действует глобально:
const PartialUser = partial(
object({
name: string(),
age: number(),
})
)
Разница проявляется особенно сильно при больших схемах.
optional управляет точечно, partial —
массово.
Partial-объекты не рекурсивны автоматически в смысле глубокого преобразования. Это ключевой момент, который часто вызывает неправильные ожидания.
const Address = object({
city: string(),
zip: number(),
})
const User = object({
name: string(),
address: Address,
})
const PartialUser = partial(User)
Результат:
name становится необязательнымaddress становится необязательнымaddress передан, он обязан быть валидным
объектом AddressЭто означает:
PartialUser.create({
address: {} // ошибка: city и zip всё ещё обязательны внутри Address
})
Partial не «спускается» внутрь вложенных объектов. Он работает только на уровне первого слоя структуры.
Partial не изменяет поведение массивов и вложенных структур.
import { array, string, number } from 'superstruct'
const Group = object({
users: array(string()),
count: number(),
})
const PartialGroup = partial(Group)
Теперь:
PartialGroup.create({})
PartialGroup.create({ users: ['a', 'b'] })
Но при этом массив остаётся строго типизированным:
PartialGroup.create({ users: [123] }) // ошибка
Partial не влияет на элементы массива, только на само поле
users.
Одно из основных применений partial-объектов — реализация PATCH-запросов в API.
Если есть ресурс:
const Post = object({
title: string(),
content: string(),
likes: number(),
})
То полное обновление требует всех полей:
Post.create({
title: 'A',
content: 'B',
likes: 10,
})
Но частичное обновление:
const PatchPost = partial(Post)
Теперь допустимы операции вида:
PatchPost.create({ title: 'New title' })
PatchPost.create({ likes: 11 })
Это отражает реальную модель серверных API, где PATCH не требует полного объекта.
Superstruct интегрируется с TypeScript через вывод типов.
import { Infer, object, string, number, partial } from 'superstruct'
const User = object({
name: string(),
age: number(),
})
type UserType = Infer<typeof User>
type PartialUserType = Infer<typeof partial(User)>
Результат:
type UserType = {
name: string
age: number
}
type PartialUserType = {
name?: string
age?: number
}
Partial автоматически преобразует все поля в optional-типы на уровне TypeScript, сохраняя строгую связь между рантайм-валидацией и типами.
Partial различает три состояния:
undefinedВ Superstruct отсутствие поля и undefined часто
трактуются по-разному в зависимости от структуры.
Для partial-объектов:
PartialUser.create({ name: undefined })
Такой случай может пройти валидацию, если undefined
допустим в контексте структуры, но чаще используется явное отсутствие
ключа.
Это важно при работе с JSON, где undefined обычно
удаляется при сериализации.
Partial можно комбинировать с другими модификаторами:
import { refine } from 'superstruct'
const PositiveNumber = refine(number(), 'PositiveNumber', (value) => {
return value > 0
})
const Struct = partial(
object({
score: PositiveNumber,
})
)
Здесь поле score может отсутствовать, но если
присутствует — обязано быть положительным числом.
Перед применением partial часто используется сокращение структуры:
import { pick } from 'superstruct'
const Base = object({
a: string(),
b: number(),
c: string(),
})
const Selected = pick(Base, ['a', 'b'])
const PartialSelected = partial(Selected)
Так формируется частичная версия уже ограниченного набора полей.
При нарушении правил partial-структуры ошибки сохраняют контекст исходной схемы.
PartialUser.create({ name: 123 })
Ошибка будет указывать, что поле name должно быть
строкой, несмотря на partial-обёртку. Это связано с тем, что partial не
изменяет валидаторы, а только их обязательность.
Типичная ошибка разработчиков — ожидание, что partial «смягчает» проверки. На практике он изменяет только presence constraint.
Partial имеет несколько принципиальных ограничений:
Эти ограничения делают поведение предсказуемым и позволяют избегать неявных преобразований данных.
Partial-объекты чаще всего используются в следующих ситуациях:
Во всех этих случаях ключевым является сохранение строгой валидации при отсутствии требований к полноте данных.
В сложных приложениях partial часто становится частью цепочки трансформаций схем:
const Base = object({
id: number(),
name: string(),
meta: object({
created: string(),
}),
})
const Update = partial(Base)
Такая композиция позволяет переиспользовать одну и ту же структуру для разных уровней строгости без дублирования описаний.
Partial в этом контексте выступает как слой адаптации между строгой доменной моделью и гибкими входными данными.