Проверка уникальности элементов

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

В библиотеке Yup работа с массивами строится через array()-схему, внутри которой описывается структура элементов с помощью of():

import * as Yup from 'yup';

const schema = Yup.array().of(
  Yup.string()
);

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


Проверка уникальности примитивных значений

Для массивов примитивов (строки, числа, булевы значения) наиболее прямолинейный способ проверки уникальности реализуется через кастомный тест:

const schema = Yup.array()
  .of(Yup.string())
  .test(
    'unique',
    'Элементы массива должны быть уникальными',
    (value) => {
      if (!value) return true;
      return new Set(value).size === value.length;
    }
  );

Использование Set позволяет быстро исключить дубликаты за счёт хранения только уникальных значений.


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

Часто требуется учитывать не «сырые» значения, а нормализованные: без пробелов, с приведением регистра:

const schema = Yup.array()
  .of(Yup.string())
  .test('unique-normalized', 'Повторяющиеся значения', (value) => {
    if (!value) return true;

    const normalized = value.map(v => v.trim().toLowerCase());
    return new Set(normalized).size === normalized.length;
  });

Такой подход устраняет ложные различия между "Test", " test " и "TEST".


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

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

const schema = Yup.array()
  .of(
    Yup.object({
      id: Yup.number().required(),
      name: Yup.string().required()
    })
  )
  .test('unique-id', 'ID должны быть уникальными', (value) => {
    if (!value) return true;

    const ids = value.map(item => item.id);
    return new Set(ids).size === ids.length;
  });

Проверка уникальности по составному ключу

Иногда один идентификатор недостаточен, и уникальность формируется комбинацией полей:

const schema = Yup.array()
  .of(
    Yup.object({
      category: Yup.string().required(),
      code: Yup.string().required()
    })
  )
  .test('unique-composite', 'Дубликаты комбинаций недопустимы', (value) => {
    if (!value) return true;

    const signatures = value.map(
      item => `${item.category}:${item.code}`
    );

    return new Set(signatures).size === signatures.length;
  });

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


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

При валидации массивов полезно фиксировать источник ошибки через createError:

const schema = Yup.array().of(
  Yup.string().test('unique-context', function (value) {
    const { path, parent } = this;

    const normalized = parent.map(v => v.toLowerCase());
    const duplicates = normalized.filter((v, i, arr) => arr.indexOf(v) !== i);

    if (duplicates.length > 0) {
      return this.createError({
        path,
        message: 'Найдены дублирующиеся значения'
      });
    }

    return true;
  })
);

Здесь используется контекст выполнения теста для более точного контроля ошибки.


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

Для повторного использования логики создаётся расширение схемы:

Yup.addMethod(Yup.array, 'unique', function (message, mapper = v => v) {
  return this.test('unique', message, function (list) {
    if (!list) return true;

    const mapped = list.map(mapper);
    return new Set(mapped).size === mapped.length;
  });
});

Использование:

const schema = Yup.array()
  .of(Yup.string())
  .unique('Значения должны быть уникальными');

Или для объектов:

const schema = Yup.array()
  .of(Yup.object({ id: Yup.number() }))
  .unique('Дубли ID', item => item.id);

Условная уникальность

При наличии зависимостей от внешних значений применяется контекст:

const schema = Yup.array().test(
  'unique-conditional',
  'Ошибка уникальности',
  function (value) {
    const { options } = this;
    const strictMode = options.context?.strict;

    if (!value) return true;

    const normalized = strictMode
      ? value
      : value.map(v => v.toLowerCase());

    return new Set(normalized).size === normalized.length;
  }
);

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


Комбинация с transform

Предобработка данных перед валидацией снижает сложность тестов:

const schema = Yup.array()
  .transform((value) =>
    value?.map(v => typeof v === 'string' ? v.trim() : v)
  )
  .test('unique', 'Дубликаты', (value) => {
    if (!value) return true;
    return new Set(value).size === value.length;
  });

Обработка вложенных структур

При сложных вложенных массивах проверка может выполняться на нескольких уровнях:

const schema = Yup.object({
  groups: Yup.array().of(
    Yup.object({
      items: Yup.array()
        .of(Yup.string())
        .test('unique', 'Дубликаты внутри группы', (value) => {
          if (!value) return true;
          return new Set(value).size === value.length;
        })
    })
  )
});

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

  • Проверка уникальности не выполняется автоматически и всегда требует явного test.
  • Ошибки, созданные внутри test, привязываются к пути текущего узла схемы.
  • При использовании abortEarly: false возможно накопление нескольких ошибок уникальности в разных ветках структуры.
  • Для больших массивов операция через Set остаётся наиболее производительной по сравнению с filter и indexOf.

Сравнение подходов проверки

Использование indexOf:

value.filter((v, i, arr) => arr.indexOf(v) === i)

Использование Set:

new Set(value).size === value.length

Второй вариант обеспечивает линейную сложность и предпочтителен при больших объёмах данных.


Проверка уникальности с игнорированием пустых значений

const schema = Yup.array().test('unique-ignore-empty', function (value) {
  if (!value) return true;

  const filtered = value.filter(v => v !== null && v !== undefined && v !== '');
  return new Set(filtered).size === filtered.length;
});

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