Уникальность элементов

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

Массив сам по себе описывается через array(struct):

import { array, string } from "superstruct";

const Tags = array(string());

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

Использование refine для ограничения повторений

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

Проверка уникальности через Set является наиболее прямолинейной реализацией:

import { array, string, refine } from "superstruct";

const UniqueTags = refine(array(string()), "UniqueTags", (value) => {
  return new Set(value).size === value.length;
});

Логика здесь опирается на свойство Set, автоматически удаляющее дубликаты. Если количество элементов множества меньше длины массива, значит присутствуют повторения.

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

Уникальность объектов по ключу

Сложность возрастает при работе с массивами объектов. Простая проверка через Set перестает быть достаточной, поскольку объекты сравниваются по ссылке, а не по содержимому.

Рассмотрим структуру:

import { array, object, string, refine } from "superstruct";

const User = object({
  id: string(),
  name: string()
});

Теперь массив пользователей:

const UniqueUsers = refine(array(User), "UniqueUsers", (value) => {
  const ids = value.map(user => user.id);
  return new Set(ids).size === ids.length;
});

Здесь уникальность определяется через поле id, что отражает типичный сценарий работы с доменными сущностями.

Глубокая проверка уникальности

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

const UniqueUsersByCompositeKey = refine(
  array(User),
  "UniqueUsersByCompositeKey",
  (value) => {
    const keys = value.map(user => `${user.id}:${user.name}`);
    return new Set(keys).size === keys.length;
  }
);

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

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

Проверка через Set имеет ряд особенностей, которые необходимо учитывать при проектировании схем:

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

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

Нормализация данных перед проверкой

В ряде случаев требуется предварительное преобразование массива перед проверкой уникальности. Это особенно актуально при работе с пользовательским вводом:

const NormalizedUniqueTags = refine(
  array(string()),
  "NormalizedUniqueTags",
  (value) => {
    const normalized = value.map(v => v.trim().toLowerCase());
    return new Set(normalized).size === normalized.length;
  }
);

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

Комбинирование с другими ограничениями

Уникальность редко используется изолированно. Чаще она сочетается с ограничениями размера массива и типами элементов.

import { size } from "superstruct";

const UniqueLimitedTags = size(
  refine(array(string()), "UniqueTags", (value) => {
    return new Set(value).size === value.length;
  }),
  1,
  10
);

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

Повторное использование логики уникальности

Для повышения переиспользуемости создаются фабрики структур:

import { array, refine } from "superstruct";

const uniqueArray = (struct) =>
  refine(array(struct), "uniqueArray", (value) => {
    return new Set(value).size === value.length;
  });

const UniqueNumbers = uniqueArray(Number);
const UniqueStrings = uniqueArray(String);

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

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

Проверка уникальности через Set имеет линейную сложность O(n), что делает её приемлемой для большинства прикладных задач. Однако при работе с большими массивами важно учитывать:

  • стоимость нормализации данных;
  • создание промежуточных массивов (map);
  • частоту вызова валидации.

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

Диагностика ошибок уникальности

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

import { refine, array, string } from "superstruct";

const UniqueEmails = refine(array(string()), "UniqueEmails", (value) => {
  return new Set(value).size === value.length;
});

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

Уникальность как часть доменной модели

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

  • идентификаторами сущностей;
  • списками тегов;
  • наборами разрешений;
  • конфигурационными ключами.

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

Альтернативные подходы через предварительную обработку

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

const deduplicate = (arr) => [...new Set(arr)];

const preprocess = (value) => ({
  tags: deduplicate(value.tags)
});

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