В системе валидации данных ключевую роль играют объединённые типы,
позволяющие описывать значения, которые могут соответствовать одному из
нескольких структурных вариантов. В библиотеке Superstruct это
реализовано через механизм union, который становится
особенно мощным при вложенной композиции.
Union в Superstruct описывает структуру, которая может быть валидной сразу по нескольким независимым схемам. Проверка выполняется последовательно: если один вариант не подходит, проверяется следующий.
import { union, string, number } from 'superstruct'
const StringOrNumber = union([string(), number()])
Такое определение допускает либо строку, либо число.
Ключевой момент заключается в том, что union не требует приведения типов — он только проверяет соответствие.
Вложенные union-типы возникают, когда внутри одного union находится другой union или сложная структура, содержащая альтернативные ветви.
Это позволяет описывать данные с многослойной вариативностью:
import { union, string, number, boolean } from 'superstruct'
const Primitive = union([
string(),
number()
])
const ExtendedPrimitive = union([
Primitive,
boolean()
])
Здесь ExtendedPrimitive является вложенным union-типом,
так как один из вариантов сам является union.
Логически это эквивалентно «объединению объединений», которое при проверке разворачивается в плоский набор вариантов.
На практике наиболее важный сценарий — использование union внутри объектов.
import { object, union, string, number } from 'superstruct'
const Admin = object({
role: string(),
permissions: array(string())
})
const User = object({
role: string(),
age: number()
})
const Account = union([Admin, User])
Здесь Account может быть либо администратором, либо
обычным пользователем.
Более сложный случай — когда union используется не на верхнем уровне, а внутри структуры:
const Response = object({
status: string(),
data: union([
object({ kind: string(), value: string() }),
object({ kind: string(), value: number() }),
object({ kind: string(), value: boolean() })
])
})
В этом примере поле data содержит вложенный union,
определяющий разные формы значения.
При усложнении модели данные могут содержать несколько уровней альтернатив:
const EventA = object({
type: string(),
payload: object({ a: string() })
})
const EventB = object({
type: string(),
payload: object({ b: number() })
})
const EventC = object({
type: string(),
payload: union([
object({ c: string() }),
object({ c: number() })
])
})
const Event = union([EventA, EventB, EventC])
Здесь присутствует двойная вложенность:
При валидации Superstruct действует по принципу последовательного перебора:
Это означает, что вложенные union увеличивают стоимость проверки, но дают высокую гибкость модели данных.
Логически вложенные union можно представить как плоский набор альтернатив:
union([
A,
B,
union([C, D])
])
эквивалентно:
union([
A,
B,
C,
D
])
Однако в коде важно сохранять структуру, потому что вложенность часто отражает смысловые границы модели.
В сложных системах вложенные union часто комбинируются с дискриминатором:
const Shape = union([
object({ type: string(), radius: number() }),
object({
type: string(),
shape: union([
object({ kind: string(), width: number() }),
object({ kind: string(), height: number() })
])
})
])
Здесь структура становится иерархической: сначала определяется форма, затем её внутренний вариант.
При работе с nested union часто возникают типовые проблемы:
1. Избыточная вложенность
Слишком глубокие union затрудняют чтение и замедляют проверку.
2. Перекрывающиеся варианты
Если структуры внутри union слишком похожи, проверка становится неоднозначной.
3. Потеря дискриминатора
Без явного различающего поля (type, kind)
выбор варианта превращается в последовательный перебор, что ухудшает
производительность.
Практическая оптимизация строится вокруг трёх принципов:
Пример:
const BaseEvent = object({
id: string(),
timestamp: number()
})
const Event = union([
object({
...BaseEvent.schema,
type: literal('A'),
payload: object({ a: string() })
}),
object({
...BaseEvent.schema,
type: literal('B'),
payload: object({ b: number() })
})
])
Особый случай — когда union ссылается на сам себя через вложенные структуры:
const Tree = union([
object({
value: string(),
children: array(Tree)
}),
object({
value: number(),
children: array(Tree)
})
])
Такая конструкция используется для деревьев, AST и графоподобных структур.
Вложенные union-типы становятся основой для моделирования:
Их сила заключается в том, что они позволяют выражать многовариантность без потери строгой типизации и без необходимости ручной проверки каждого случая.