Валидация разнородных массивов

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

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


Базовая модель массива в Superstruct

В Superstruct массив описывается через array(structure), где structure задаёт тип каждого элемента:

import { array, number } from 'superstruct';

const Numbers = array(number());

Такое определение допускает только массив чисел. Любое отклонение приведёт к ошибке валидации.

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


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

Основным инструментом работы с разнородными массивами является union. Он позволяет объединять несколько структурных типов в один допустимый набор.

import { array, union, string, number } from 'superstruct';

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

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


Поведение валидации при union в массивах

При использовании union внутри array происходит поэлементная проверка:

  1. Каждый элемент массива передаётся в union.
  2. union последовательно проверяет соответствие всем указанным структурам.
  3. Если хотя бы одна структура проходит проверку — элемент считается валидным.
  4. Если ни одна структура не подходит — валидация завершается ошибкой.

Такой подход обеспечивает гибкость, но увеличивает вычислительную стоимость валидации при больших массивах или сложных структурах.


Разнородные массивы объектов

На практике наиболее распространённый случай — массив объектов разной структуры. Например, список событий, где каждый элемент описывает свой тип действия.

import { array, union, object, string, number } from 'superstruct';

const ClickEvent = object({
  type: string(),
  x: number(),
  y: number(),
});

const ScrollEvent = object({
  type: string(),
  delta: number(),
});

const Events = array(
  union([ClickEvent, ScrollEvent])
);

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


Дискриминируемые объединения как основа типизации

При работе с разнородными массивами часто используется дискриминатор — поле, определяющее тип объекта. Обычно это type или аналогичный ключ.

const ClickEvent = object({
  type: string(),
  x: number(),
  y: number(),
});

const ScrollEvent = object({
  type: string(),
  delta: number(),
});

Однако без дополнительной проверки type остаётся обычной строкой. Для более строгой схемы применяется уточнение через literal.

import { literal } from 'superstruct';

const ClickEvent = object({
  type: literal('click'),
  x: number(),
  y: number(),
});

const ScrollEvent = object({
  type: literal('scroll'),
  delta: number(),
});

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


Вложенные разнородные массивы

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

const Data = array(
  union([
    array(number()),
    object({
      label: string(),
      values: array(number()),
    }),
  ])
);

Здесь элементы массива могут быть либо массивом чисел, либо объектом с вложенным массивом чисел. Подобные конструкции часто используются при обработке гибридных API-ответов.


Ограничения и особенности производительности

Использование union в массивах влияет на производительность в зависимости от количества альтернативных структур:

  • каждый элемент проверяется последовательно;
  • сложные вложенные структуры увеличивают время валидации;
  • отсутствие дискриминатора приводит к полному перебору вариантов.

Для оптимизации часто используется:

  • упрощение структур;
  • введение строгого дискриминирующего поля;
  • разделение массивов на несколько отдельных структур.

Разделение массивов вместо union

В ряде случаев более эффективным решением является отказ от разнородного массива в пользу нескольких однородных:

const Data = object({
  numbers: array(number()),
  strings: array(string()),
});

Такой подход снижает сложность валидации и повышает предсказуемость структуры данных, особенно при больших объёмах информации.


Кастомные проверки в разнородных массивах

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

import { refine } from 'superstruct';

const PositiveNumber = refine(number(), 'PositiveNumber', (value) => {
  return value > 0;
});

В сочетании с union это позволяет создавать более точные ограничения:

const Mixed = array(
  union([string(), PositiveNumber])
);

Работа с неизвестными структурами

При обработке внешних данных часто встречаются случаи, когда структура массива заранее неизвестна. В таких ситуациях используется комбинация any и последующей валидации:

import { any } from 'superstruct';

const LooseArray = array(any());

Далее элементы могут быть преобразованы и проверены отдельно. Такой подход применяется как промежуточный слой при интеграции с внешними API.


Практические паттерны построения схем

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

  • минимизация числа альтернатив в union;
  • использование дискриминаторов;
  • избегание глубокой вложенности union внутри union;
  • предпочтение объектной структуры вместо полностью свободных массивов.

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


Комбинирование массивов и структурных ограничений

Разнородные массивы часто используются вместе с дополнительными ограничениями на сам массив:

import { size } from 'superstruct';

const LimitedMixedArray = size(
  array(union([string(), number()])),
  1,
  10
);

Здесь задаётся ограничение на количество элементов, что дополнительно снижает риск неконтролируемого роста данных.


Обработка ошибок валидации

При нарушении структуры разнородного массива Superstruct возвращает детализированное описание ошибки, включая:

  • индекс элемента массива;
  • ожидаемые типы;
  • фактическое значение.

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