Tuple с переменной длиной

В Superstruct кортежи представляют собой строго типизированные массивы, где позиция каждого элемента имеет значение. В отличие от обычных массивов, где элементы могут быть произвольными и однородными, кортежи фиксируют структуру данных на уровне индексов. Однако реальная разработка часто требует более гибкого подхода — когда часть структуры фиксирована, а оставшиеся элементы могут варьироваться по количеству и типу.

Базовая модель кортежа в Superstruct задаётся через перечисление структур для каждого индекса:

import { tuple, string, number } from "superstruct";

const UserTuple = tuple([string(), number()]);

Такое определение означает строгую схему:

  • первый элемент должен быть строкой
  • второй элемент должен быть числом
  • длина массива строго равна двум

Любое отклонение от структуры приводит к ошибке валидации:

UserTuple(["Alice", 25]); // валидно
UserTuple(["Alice"]); // ошибка
UserTuple(["Alice", 25, true]); // ошибка

Такой подход полезен, когда структура данных полностью известна заранее: координаты, пары значений, фиксированные записи протоколов.

Ограничения фиксированных кортежей

Жёсткая длина становится проблемой, когда структура данных частично динамическая. Например:

  • список тегов после обязательных полей
  • аргументы функции с фиксированным началом и переменным хвостом
  • протоколы, где заголовок фиксирован, а тело переменно

В таких случаях требуется механизм расширения кортежа.

Переменная длина через rest-структуру

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-части

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

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] — полезная нагрузка переменной длины

Типизация в TypeScript

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 возвращает объект ошибки, где особенно важно различать:

  • ошибка фиксированной части
  • ошибка rest-части

Пример:

Struct(["ok", 10, true]);

Ошибка будет указывать, что элемент rest не соответствует типу string.

При сложных структурах полезно анализировать путь ошибки (path), который показывает индекс:

  • path: [2] — ошибка в первом элементе rest
  • path: [0] — ошибка в фиксированной части

Производительность при больших кортежах

Переменные кортежи могут содержать большое количество элементов в rest-части. Важно учитывать, что:

  • фиксированная часть проверяется быстро и линейно по длине
  • rest-часть проверяется последовательно для каждого элемента
  • сложные union-типы внутри rest увеличивают стоимость проверки

При больших массивах рекомендуется избегать глубоких union-структур в хвосте.

Комбинация с optional и defaults

Хотя кортежи по своей природе позиционные, их можно комбинировать с логикой опциональности через предварительную нормализацию данных:

const Flexible = tuple([
  string(),
  number(),
], array(string()));

Перед валидацией можно приводить входные данные к унифицированному виду, если часть параметров может отсутствовать.

Использование в доменных моделях

Переменные кортежи особенно полезны при моделировании систем, где есть:

  • заголовок + payload
  • метаданные + список событий
  • команда + аргументы
  • ключ + цепочка значений

Пример доменной модели событий:

const Event = tuple([
  string(), // тип события
  number(), // timestamp
], array(string()));

Такая структура позволяет унифицировать поток событий, сохраняя строгую структуру первых полей и гибкость последующих.

Ограничения подхода

Несмотря на гибкость, переменные кортежи имеют ограничения:

  • сложнее читаются при большом количестве уровней вложенности
  • требуют строгого контроля типов rest-части
  • плохо подходят для полностью динамических структур без фиксированного префикса

Если структура данных не имеет стабильного начала, лучше использовать object-структуры вместо кортежей.

Композиция с другими структурами Superstruct

Кортежи с переменной длиной часто комбинируются с другими примитивами:

  • array() для однородных списков
  • object() для именованных полей
  • union() для альтернативных форматов

Комбинированные схемы позволяют описывать сложные протоколы, сохраняя читаемость на уровне кода.

const Message = union([
  tuple([string(), string()]),
  tuple([string(), number()], array(string())),
]);

Такая модель позволяет описывать несколько форматов сообщений в одной структуре.