Валидация данных редко ограничивается единственным допустимым типом.
На практике поля часто могут принимать несколько различных форм: строку
или число, объект одного из нескольких типов, либо одну из заранее
заданных структур. Для описания таких сценариев в Superstruct
используется комбинатор union, позволяющий задать набор
альтернативных структур, из которых хотя бы одна должна быть
валидной.
union объединяет несколько структур в одну логическую
проверку. Значение считается корректным, если оно соответствует
хотя бы одной из заданных структур.
Формально:
S1, S2, S3...Базовый синтаксис:
import { union, string, number } from 'superstruct'
const StringOrNumber = union([string(), number()])
В этом примере допустимыми значениями будут строки и числа.
import { union, string, number, validate } from 'superstruct'
const Struct = union([string(), number()])
validate('hello', Struct) // ok
validate(123, Struct) // ok
validate(true, Struct) // ошибка
Union работает как логическое «ИЛИ» между структурами.
const BoolOrString = union([string(), boolean()])
validate('ok', BoolOrString) // ok
validate(false, BoolOrString) // ok
validate(10, BoolOrString) // ошибка
Наиболее частое применение union — различие между
структурами объектов.
import { union, object, string, number } from 'superstruct'
const Admin = object({
role: string(),
permissions: string()
})
const User = object({
username: string(),
age: number()
})
const Person = union([Admin, User])
Теперь валидатор принимает либо администратора, либо обычного пользователя.
Если несколько структур могут одновременно подойти под одно значение,
union выберет первую подходящую.
const A = object({ type: string() })
const B = object({ type: string(), extra: string() })
const U = union([A, B])
Значение:
{ type: 'x', extra: 'y' }
будет валидировано как A, потому что проверка идёт
последовательно.
Это важно учитывать при проектировании: более общие структуры следует размещать после более специфичных.
Если ни одна структура не подходит, возвращается агрегированная ошибка.
validate(123, union([string(), boolean()]))
Результат содержит информацию о каждой неудачной попытке проверки.
Внутренне формируется список ошибок от каждой структуры, что позволяет понять причину несоответствия.
Union можно вкладывать внутрь других структур:
const Data = object({
value: union([string(), number(), boolean()])
})
Теперь поле value может быть любого из трёх типов.
const StringArray = array(string())
const NumberArray = array(number())
const ArrayUnion = union([StringArray, NumberArray])
Допустимы:
['a', 'b', 'c']
[1, 2, 3]
Но смешанные массивы:
['a', 1]
не пройдут проверку.
Один из самых мощных паттернов — использование дискриминатора (обычно
поля type).
const Dog = object({
type: literal('dog'),
barkVolume: number()
})
const Cat = object({
type: literal('cat'),
livesLeft: number()
})
const Animal = union([Dog, Cat])
Здесь поле type выступает маркером выбора структуры.
validate({ type: 'dog', barkVolume: 10 }, Animal) // ok
validate({ type: 'cat', livesLeft: 7 }, Animal) // ok
validate({ type: 'dog', livesLeft: 7 }, Animal) // ошибка
Такой подход снижает неоднозначность и повышает производительность проверки.
Поскольку union проверяет структуры последовательно,
порядок влияет на скорость.
Рекомендуемые принципы:
Пример плохой организации:
union([
object({ value: string() }),
object({ value: string(), extra: number() })
])
Второй вариант никогда не будет достигнут.
Правильный вариант:
union([
object({ value: string(), extra: number() }),
object({ value: string() })
])
union не только проверяет, но и может участвовать в
трансформации, если используются соответствующие структуры.
import { coerce, string, number, union } from 'superstruct'
const Struct = union([
coerce(number(), string(), value => Number(value)),
number()
])
Теперь строка "42" преобразуется в число
42.
С ростом вложенности увеличивается стоимость проверки:
const Struct = union([
object({
type: literal('a'),
data: union([string(), number()])
}),
object({
type: literal('b'),
data: array(union([string(), number(), boolean()]))
})
])
Такие конструкции допустимы, но требуют аккуратного контроля структуры данных.
Если структуры неразличимы, результат становится непредсказуемым.
Без явного поля выбора объектные union могут давать неоднозначные совпадения.
Глубокие union замедляют валидацию и усложняют сопровождение.
В системах типизации и валидации union часто сравнивают с аналогами:
В Superstruct union остаётся простым примитивом без
скрытой магии: каждая структура проверяется явно, последовательно и
независимо.
Union можно комбинировать с дополнительными проверками:
import { union, string, refine } from 'superstruct'
const ShortString = refine(string(), value => value.length < 10)
const Struct = union([ShortString, string()])
Хотя здесь есть избыточность, подобные конструкции применяются для ограничения одной из веток union.
На практике union часто используется для:
Пример событий:
const Created = object({
event: literal('created'),
id: string()
})
const Deleted = object({
event: literal('deleted'),
id: string(),
reason: string()
})
const Event = union([Created, Deleted])
В системах с большим количеством вариантов часто используется комбинация:
Пример конфигурационного DSL:
const Config = union([
object({ mode: literal('dev'), debug: boolean() }),
object({ mode: literal('prod'), cache: boolean() }),
object({ mode: literal('test'), seed: number() })
])
Такой подход обеспечивает строгую типизацию различных режимов работы системы без потери гибкости.
При добавлении новых вариантов важно учитывать обратную совместимость:
В противном случае поведение может измениться из-за приоритета первой подходящей структуры.