Циклические зависимости

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

На уровне логики это проявляется как ситуация, в которой объект A требует валидации объекта B, а объект B — объекта A. При попытке немедленного вычисления таких схем происходит либо бесконечная рекурсия, либо ошибка переполнения стека, либо частично инициализированная структура, приводящая к некорректной валидации.


Механизм возникновения в схемах Yup

Yup строит схемы декларативно, но вычисление некоторых узлов происходит лениво только в ограниченном числе случаев. Если схема описана напрямую через объектные ссылки, JavaScript выполняет её сразу при импорте модуля или создании объекта схемы.

Типичный источник проблемы — взаимные ссылки в объектах:

import * as yup from 'yup';

const schemaA = yup.object({
  b: schemaB
});

const schemaB = yup.object({
  a: schemaA
});

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

В результате:

  • либо возникает ReferenceError
  • либо создаётся неполная схема
  • либо появляется бесконечная рекурсия при валидации

Ленивая инициализация через yup.lazy

Основной механизм разрыва циклов в Yup — использование yup.lazy. Эта функция позволяет отложить вычисление схемы до момента фактической валидации значения.

import * as yup from 'yup';

const schemaA = yup.object({
  b: yup.lazy(() => schemaB)
});

const schemaB = yup.object({
  a: yup.lazy(() => schemaA)
});

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

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


Взаимные ссылки между объектами

Часто циклы возникают в моделях данных, где сущности взаимосвязаны:

  • пользователь → профиль → пользователь
  • комментарий → пост → комментарии
  • узел дерева → родитель → дети → узлы

В Yup такие структуры требуют осторожного моделирования.

Пример рекурсивного дерева:

import * as yup from 'yup';

const node = yup.object({
  id: yup.string().required(),
  children: yup.array().of(
    yup.lazy(() => node)
  )
});

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


Циклы в массивах и рекурсивные структуры

Массивы часто становятся естественной точкой возникновения рекурсии. Особенно это характерно для структур:

  • файловые системы
  • меню навигации
  • организационные деревья

При прямом описании без lazy возникает немедленное развертывание схемы:

const menu = yup.object({
  title: yup.string(),
  items: yup.array().of(menu)
});

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

Корректная форма:

const menu = yup.object({
  title: yup.string(),
  items: yup.array().of(
    yup.lazy(() => menu)
  )
});

Циклы на уровне модулей ES

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

При использовании структуры файлов:

schemaA.js -> schemaB.js -> schemaA.js

происходит частичная загрузка модулей. В JavaScript это выражается в следующем:

  • один модуль получает ссылку на ещё не инициализированный экспорт
  • значение экспортируемой переменной временно становится undefined
  • Yup получает некорректную схему

Пример:

// schemaA.js
import { schemaB } from './schemaB.js';

export const schemaA = yup.object({
  b: yup.lazy(() => schemaB)
});
// schemaB.js
import { schemaA } from './schemaA.js';

export const schemaB = yup.object({
  a: yup.lazy(() => schemaA)
});

Даже при использовании lazy проблема может сохраняться, если сам модуль импортируется до завершения инициализации.


Разрыв циклов через архитектурное разделение

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

Распространённые методы:

1. Выделение общих под-схем

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

export const baseUser = yup.object({
  id: yup.string().required()
});

2. Использование фабрик схем

Схема создаётся как функция, а не как статический объект:

export const createUserSchema = () =>
  yup.object({
    friend: yup.lazy(() => createUserSchema())
  });

3. Локализация зависимостей

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


Рекурсивные типы и ограничения Yup

Несмотря на гибкость lazy, существуют ограничения:

  • невозможность бесконечно глубокой рекурсии без контроля
  • риск stack overflow при некорректных данных
  • усложнение типизации в TypeScript
  • снижение читаемости схем при глубокой вложенности

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


Особенности поведения при валидации циклов

При наличии циклических структур в данных возможны следующие эффекты:

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

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


Практика безопасного моделирования циклических схем

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

  • минимизация взаимных ссылок
  • явное управление рекурсией через lazy
  • разделение уровней данных (DTO vs доменные структуры)
  • контроль глубины вложенности на уровне схем

Пример ограничения глубины:

const tree = yup.object({
  value: yup.string(),
  depth: yup.number(),
  children: yup.lazy((value) => {
    if (value && value.depth > 5) {
      return yup.mixed().strip();
    }
    return yup.array().of(tree);
  })
});

Поведение при сложных графах объектов

Когда структура данных представляет собой граф, а не дерево, Yup становится менее предсказуемым инструментом. В графах:

  • узлы могут иметь несколько родителей
  • возможны циклы произвольной длины
  • повторное посещение объектов неизбежно

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

const graphNode = yup.object({
  id: yup.string(),
  parentIds: yup.array(yup.string())
});

Такая модель полностью устраняет циклы на уровне структуры.


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

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

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

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