В Superstruct кортежи представляют собой строго типизированные массивы, где позиция каждого элемента имеет значение. В отличие от обычных массивов, где элементы могут быть произвольными и однородными, кортежи фиксируют структуру данных на уровне индексов. Однако реальная разработка часто требует более гибкого подхода — когда часть структуры фиксирована, а оставшиеся элементы могут варьироваться по количеству и типу.
Базовая модель кортежа в Superstruct задаётся через перечисление структур для каждого индекса:
import { tuple, string, number } from "superstruct";
const UserTuple = tuple([string(), number()]);
Такое определение означает строгую схему:
Любое отклонение от структуры приводит к ошибке валидации:
UserTuple(["Alice", 25]); // валидно
UserTuple(["Alice"]); // ошибка
UserTuple(["Alice", 25, true]); // ошибка
Такой подход полезен, когда структура данных полностью известна заранее: координаты, пары значений, фиксированные записи протоколов.
Жёсткая длина становится проблемой, когда структура данных частично динамическая. Например:
В таких случаях требуется механизм расширения кортежа.
Superstruct решает задачу расширяемых кортежей через концепцию остаточного сегмента (rest). Идея заключается в том, что часть элементов фиксируется, а оставшиеся валидируются по отдельному правилу.
Синтаксически это выглядит как комбинация кортежа и дополнительного структурного правила для хвоста.
import { tuple, string, number, array } from "superstruct";
const LogEntry = tuple([
string(), // уровень логирования
string(), // сообщение
], array(string()));
Здесь логика следующая:
Пример использования:
LogEntry(["INFO", "Server started"]);
LogEntry(["ERROR", "Crash", "userService", "timeout", "retry"]);
Во втором случае:
"INFO" и "ERROR" — уровень"Server started" и "Crash" —
сообщениеRest-структура применяется ко всем элементам после фиксированной границы. Важно понимать, что она не ограничивает минимальную длину автоматически — минимальная длина определяется количеством фиксированных элементов.
Если задать:
tuple([string(), number()], array(boolean()));
то допустимы:
["id", 10] // валидно, rest пуст
["id", 10, true] // валидно
["id", 10, true, false] // валидно
Но:
["id"] // ошибка (не хватает number())
Переменные кортежи часто используются для описания командных интерфейсов:
const Command = tuple([
string(), // имя команды
], array(any()));
Примеры:
Command(["exit"]);
Command(["set", "theme", "dark"]);
Command(["move", 10, 20, "fast"]);
Такой подход позволяет описывать CLI-подобные структуры без потери типовой строгости для начальной части команды.
Rest-часть не обязана быть однородной в широком смысле логики приложения, но в Superstruct она должна соответствовать одной структуре. Для сложных сценариев используется union внутри rest:
import { tuple, string, number, union } from "superstruct";
const MixedTuple = tuple([
string(),
], union([string(), number()]));
Это позволяет допустить хвост из значений двух типов:
MixedTuple(["data", "extra"]);
MixedTuple(["data", 123]);
MixedTuple(["data", "extra", "more"]);
Однако такой подход требует осторожности: логика обработки становится менее предсказуемой.
Кортежи могут быть вложенными, включая комбинации фиксированных и расширяемых частей:
const Packet = tuple([
string(), // тип пакета
tuple([
number(), // версия
string(), // идентификатор
]),
], array(number()));
Пример:
Packet(["DATA", [1, "abc"], 10, 20, 30]);
Здесь структура интерпретируется так:
"DATA" — тип[1, "abc"] — заголовок[10, 20, 30] — полезная нагрузка переменной длиныSuperstruct интегрируется с TypeScript, и кортежи с rest корректно выводят типы:
import { Infer, tuple, string, number, array } from "superstruct";
const Struct = tuple([string(), number()], array(string()));
type StructType = Infer<typeof Struct>;
Тип будет:
[string, number, ...string[]]
Это важно для строгой типизации функций, принимающих переменные кортежи.
При нарушении структуры Superstruct возвращает объект ошибки, где особенно важно различать:
Пример:
Struct(["ok", 10, true]);
Ошибка будет указывать, что элемент rest не соответствует типу
string.
При сложных структурах полезно анализировать путь ошибки
(path), который показывает индекс:
path: [2] — ошибка в первом элементе restpath: [0] — ошибка в фиксированной частиПеременные кортежи могут содержать большое количество элементов в rest-части. Важно учитывать, что:
При больших массивах рекомендуется избегать глубоких union-структур в хвосте.
Хотя кортежи по своей природе позиционные, их можно комбинировать с логикой опциональности через предварительную нормализацию данных:
const Flexible = tuple([
string(),
number(),
], array(string()));
Перед валидацией можно приводить входные данные к унифицированному виду, если часть параметров может отсутствовать.
Переменные кортежи особенно полезны при моделировании систем, где есть:
Пример доменной модели событий:
const Event = tuple([
string(), // тип события
number(), // timestamp
], array(string()));
Такая структура позволяет унифицировать поток событий, сохраняя строгую структуру первых полей и гибкость последующих.
Несмотря на гибкость, переменные кортежи имеют ограничения:
Если структура данных не имеет стабильного начала, лучше использовать
object-структуры вместо кортежей.
Кортежи с переменной длиной часто комбинируются с другими примитивами:
array() для однородных списковobject() для именованных полейunion() для альтернативных форматовКомбинированные схемы позволяют описывать сложные протоколы, сохраняя читаемость на уровне кода.
const Message = union([
tuple([string(), string()]),
tuple([string(), number()], array(string())),
]);
Такая модель позволяет описывать несколько форматов сообщений в одной структуре.