В системах валидации данных одной из частых задач становится обеспечение уникальности элементов внутри коллекций. В контексте Superstruct это реализуется не через отдельный встроенный тип, а через композицию базовых структур и пользовательских правил проверки.
Массив сам по себе описывается через array(struct):
import { array, string } from "superstruct";
const Tags = array(string());
Такой структурой допускается любой набор строк, включая повторяющиеся значения. Если требуется гарантировать отсутствие дублей, вводится дополнительная проверка.
Основной механизм расширения логики в 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 имеет ряд особенностей, которые
необходимо учитывать при проектировании схем:
Эти особенности могут приводить к логическим расхождениям, если схема не учитывает специфику данных.
В ряде случаев требуется предварительное преобразование массива перед проверкой уникальности. Это особенно актуально при работе с пользовательским вводом:
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)
});
Такой метод переносит ответственность за уникальность на этап подготовки данных, снижая нагрузку на схему, но усложняя контроль целостности внутри валидации.