В Superstruct объединения (union) используются для описания значений, которые могут соответствовать одному из нескольких структурных типов. Однако при работе со сложными объектами возникает необходимость однозначно определять, к какому варианту относится конкретный объект. Для этого применяется подход, известный как discriminated unions — дискриминируемые объединения.
Ключевая идея заключается в наличии общего поля-дискриминатора, которое однозначно указывает на конкретную ветку структуры.
В библиотеке Superstruct объединение строится с помощью функции
union, которая принимает массив структур:
import { union, string, number, object } from 'superstruct';
const StringOrNumber = union([string(), number()]);
Такое определение позволяет значению быть либо строкой, либо числом. Проверка происходит последовательно: каждый структурный вариант тестируется до первого совпадения.
Особенность такого подхода — отсутствие контекста выбора ветки. Каждый вариант проверяется независимо, что при сложных объектах может снижать эффективность и усложнять диагностику ошибок.
Дискриминируемое объединение вводит явный маркер типа. Обычно это
строковое поле, например type, kind или
tag.
Структура каждого варианта содержит фиксированное значение дискриминатора:
import { object, string, number, literal } from 'superstruct';
const Cat = object({
type: literal('cat'),
meows: number(),
});
const Dog = object({
type: literal('dog'),
barks: number(),
});
Здесь поле type выполняет роль дискриминатора. Оно
позволяет однозначно определить структуру без попытки проверки всех
вариантов.
Дискриминируемое объединение в Superstruct формируется через обычный
union, где каждый вариант уже содержит уникальный
литерал-дискриминатор:
import { union } from 'superstruct';
const Animal = union([Cat, Dog]);
При валидации происходит следующее:
type.Такой подход уменьшает количество проверок и повышает предсказуемость ошибок.
Функция literal играет ключевую роль, фиксируя значение
поля:
import { literal } from 'superstruct';
literal('cat')
В отличие от string(), который допускает любые строки,
literal ограничивает значение строго заданной константой.
Это создаёт стабильный маркер, на который опирается механизм выбора
ветки.
При вызове validate, assert или
create происходит автоматический выбор подходящей
ветки:
import { validate } from 'superstruct';
const value = {
type: 'dog',
barks: 3,
};
const [error, result] = validate(value, Animal);
Алгоритм работы:
literal значениями;Если дискриминатор отсутствует или не совпадает ни с одной веткой, возвращается ошибка валидации.
Каждая ветка в discriminated union должна иметь:
Пример расширенного случая:
const Shape = union([
object({
kind: literal('circle'),
radius: number(),
}),
object({
kind: literal('rectangle'),
width: number(),
height: number(),
}),
object({
kind: literal('square'),
size: number(),
}),
]);
Такое представление позволяет формировать строго типизированные и легко расширяемые модели данных.
Дискриминируемые объединения могут быть вложенными. Это используется для моделирования сложных доменных структур.
const Event = union([
object({
type: literal('mouse'),
action: union([
object({
subtype: literal('click'),
x: number(),
y: number(),
}),
object({
subtype: literal('move'),
x: number(),
y: number(),
}),
]),
}),
]);
Здесь происходит многоуровневая дискриминация: сначала по
type, затем по subtype.
На практике часто встречаются ошибки, связанные с нарушением структуры дискриминируемых объединений:
1. Отсутствие уникального значения
Если два варианта используют одинаковый literal, выбор
ветки становится неоднозначным.
2. Использование изменяемого поля
Дискриминатор должен быть неизменяемым значением. Использование вычисляемых или опциональных значений приводит к нестабильной валидации.
3. Несогласованность структуры
Если ветки имеют разную семантику одного и того же поля, например
type, но с разным смыслом, модель становится трудно
поддерживаемой.
Discriminated unions в Superstruct позволяют сократить количество проверок за счёт раннего выбора ветки. Вместо перебора всех структур используется логика:
Это особенно важно при больших схемах с десятками вариантов.
Ветви discriminated union могут строиться из переиспользуемых структурных блоков:
const Timestamped = object({
createdAt: string(),
});
const UserEvent = object({
type: literal('user_event'),
userId: number(),
...Timestamped.schema,
});
Такой подход уменьшает дублирование и делает структуру более модульной, сохраняя при этом дискриминацию на уровне верхнего поля.
Дискриминируемые объединения часто применяются при обработке внешних данных:
Типичный сценарий:
const Message = union([
object({
kind: literal('text'),
content: string(),
}),
object({
kind: literal('image'),
url: string(),
}),
]);
Каждое сообщение однозначно классифицируется, что упрощает обработку на уровне бизнес-логики.
Добавление новых вариантов не требует изменения существующих структур. Достаточно добавить новую ветку с уникальным discriminator:
const ExtendedMessage = union([
...Message.types,
object({
kind: literal('video'),
url: string(),
}),
]);
Такая модель хорошо масштабируется при росте системы.