Failures и их свойства

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

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

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

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

Структура failure-объекта

Каждый failure в Superstruct описывает конкретную точку несоответствия данных ожидаемой схеме. Структура объекта включает несколько ключевых полей:

path

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

Пример:

path: ['user', 'address', 'zip']

Такой путь означает, что ошибка произошла в поле zip, находящемся внутри address, который, в свою очередь, является частью объекта user.

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

value

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

Пример:

value: "12AB"

type

type указывает на базовый тип структуры, который ожидался в момент проверки. Это может быть string, number, array, object или пользовательский struct.

Важно понимать, что type отражает не JavaScript-тип, а именно тип, заданный в системе Superstruct.

refinement

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

Пример:

refinement: 'positiveNumber'

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

message

message — текстовое описание ошибки. Оно может генерироваться автоматически или задаваться вручную при создании структуры.

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

branch

branch — полный путь значений от корня структуры до текущего узла, включая промежуточные объекты. В отличие от path, который хранит только ключи, branch содержит реальные данные на каждом уровне.

Пример:

branch: [{ user: { address: { zip: "12AB" } } }, { address: { zip: "12AB" } }, "12AB"]

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

expected и received

В некоторых реализациях failure-объектов присутствуют поля:

  • expected — ожидаемый тип или значение
  • received — фактическое значение или тип

Эти поля делают ошибки более читаемыми при логировании.

Агрегация failures

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

Это поведение особенно важно для UX-слоёв, где необходимо показать пользователю полный список ошибок формы, а не только первую найденную.

Пример:

{
  failures: [
    { path: ['email'], value: 'abc', type: 'string', message: 'Invalid email' },
    { path: ['age'], value: -5, type: 'number', message: 'Must be positive' }
  ]
}

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

Failures в объектах и вложенных структурах

При валидации объектов failures формируются рекурсивно. Каждый уровень вложенности добавляет свой сегмент в path и расширяет branch.

Например, при проверке структуры:

{
  user: {
    profile: {
      name: 123
    }
  }
}

Failure будет иметь путь:

['user', 'profile', 'name']

и значение:

123

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

Failures в массивах

При работе с массивами failures получают числовые индексы в path.

Пример:

['items', 2, 'price']

Это означает, что ошибка произошла у третьего элемента массива items.

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

Failures в union-структурах

Union-типы являются одной из самых сложных частей системы проверки. В случае несоответствия ни одному из вариантов union формируется набор failures от каждой ветки проверки.

Каждый failure при этом сохраняет информацию о том, какая именно ветка была проверена.

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

Формирование message и кастомизация

Сообщения в failure могут быть как автоматически сгенерированными, так и заданными вручную через описание структуры.

Автоматическая генерация основывается на:

  • ожидаемом типе
  • фактическом значении
  • контексте пути
  • имени refinement (если применимо)

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

Итерация по failures

Failures в Superstruct реализованы как итерируемая структура. Это означает, что их можно обрабатывать через for...of без преобразования в массив.

Пример обработки:

for (const failure of error.failures()) {
  console.log(failure.path, failure.message);
}

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

Нормализация failures

Внутренне Superstruct может нормализовать failures для унификации формата. Это особенно важно при объединении результатов от разных типов структур.

Нормализация включает:

  • приведение path к единому виду
  • унификацию message
  • приведение value к исходному типу без преобразований

Производственные особенности

Failures проектировались с учётом необходимости минимального overhead при массовой валидации. Поэтому:

  • данные не дублируются без необходимости
  • path строится лениво
  • branch формируется только при обращении к error-объекту

Это позволяет использовать Superstruct в высоконагруженных сценариях, таких как валидация API-запросов или потоковых данных.

Типичные паттерны использования failures

На практике failures чаще всего применяются для:

  • построения форм валидации с полным списком ошибок
  • логирования структурных проблем входных данных
  • генерации человекочитаемых отчётов о несоответствиях
  • интеграции с UI-слоем, где каждое поле связано с конкретным failure через path

Часто используется сопоставление failures с UI-полями, где path.join('.') служит ключом для отображения ошибки.

Поведение при кастомных структурах

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

Refinement-ошибки особенно часто используются в этом сценарии, поскольку они позволяют разделить базовую типизацию и бизнес-логику.

Композиция failures

При объединении структур failures не теряют контекст. Каждая вложенная структура добавляет свои данные, сохраняя общую цепочку.

Это обеспечивает прозрачную трассировку ошибок даже в сложных композициях, где одна структура включает другую через object, array или union.

Поведение при частичной валидации

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

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