Ранее публиковал теоретическую часть по рекурсивным импортам, желательно ознакомиться перед тем как продолжить, чтобы было общее преставление.
Рекурсивные импорты рассмотрим на примере React/Redux приложении.
Исходный код приложения опубликован тут, можете склонировать и попробовать самостоятельно исправить ошибки. Так сказать закрепить теорию на практике.
Barrel файлы и рекурсивные импорты
Частая ошибка, связанная с рекурсивными импортами — это использование barrel файлов. Желательно их не использовать где попало.
Проблема с barrel файлами в папке modules
Например, в modules не нужно создавать barrel файл index.ts, который реэкспортирует другие модули, т.к. может быть такое, что модули могут в рамках себя переиспользовать другие модули. Но тут есть разные варианты решения этой проблемы: если мы отказываемся от barrel файла в папке modules, мы импортируем тогда конкретный модуль.
Корректно:
import { clientActions } from "@modules/client";Некорректно:
import { clientActions } from "@modules";Относительные импорты внутри модуля
А если мы в рамках одного модуля экспортируем внутренние модули, то необходимо использовать относительный импорт.
Пример:
// Внутри модуля @modules/client
import { someHelper } from "./helpers";
import { clientTypes } from "./types";Принцип высокого зацепления (High Cohesion)
Не нужно в barrel файл @modules/client/index.ts добавлять реэкспорт наружу файлов redux.tsx и ui, если они не используются снаружи. В концепции Feature Slices Design это упоминается как high cohesion (высокое зацепление) — участки кода, логически связанные между собой, помещаются рядом.
Некорректно:
// @modules/client/index.ts
export { clientActions, clientReducer } from "./redux";
export { ClientComponent } from "./ui";
export { clientTypes } from "./types";Корректно (если redux и ui используются только внутри модуля):
// @modules/client/index.ts
// Экспортируем только публичный API модуля
export { ClientService } from "./service";
export type { Client } from "./types";Рекомендации и best practices
1. Структура модулей

2. Правила импортов
Между модулями: используйте абсолютные импорты через алиасы (
@modules/client)
// В модуле @modules/order/service.ts
import { clientActions } from "@modules/client";Внутри модуля: используйте относительные импорты (
./,../)
// В файле @modules/client/service.ts
import { clientActions } from "./redux/actions";
import { clientTypes } from "./types";
import { formatDate } from "../utils/format"; // Импорт из родительской папкиИзбегайте: barrel файлов на верхнем уровне
modules/index.tsНе используйте: импорт barrel файла внутри модуля через
'.'(он же'./index.ts') или'../'(он же'../index.ts'), используйте прямой относительный импорт. Например, в файлеservice.ts, если нам нуженredux:
Корректный импорт:
import { clientActions } from "./redux";Некорректный импорт:
import { clientActions } from "."; // Импорт через barrel файлВизуально это выглядит не очень и может создать рекурсивные импорты.
3. Когда использовать barrel файлы
Barrel файлы уместны:
Внутри модуля для экспорта публичного API
Когда нет риска циклических зависимостей
Когда модуль является конечной точкой (leaf node) в графе зависимостей
Что такое leaf node (конечная точка) в графе зависимостей?
Leaf node (листовой узел) — это модуль, который сам не импортирует другие модули проекта (или импортирует только внешние библиотеки), но может быть импортирован другими модулями. Такой модуль находится в конце цепочки зависимостей и не может создать циклическую зависимость.
Пример графа зависимостей:

В этом примере:
utils— leaf node: не зависит от других модулей проекта, только от внешних библиотекclient— зависит отutils, но не от других модулейorder— зависит отclientиutils
Пример небезопасного barrel файла:
// modules/index.ts - НЕБЕЗОПАСНО!
// Если client и order импортируют друг друга, возникнет цикл
export * from "./client";
export * from "./order";4. Обнаружение проблем
Для обнаружения рекурсивных импортов можно использовать инструменты анализа зависимостей:
[
madge](https://github.com/pahen/madge) — визуализация графа зависимостей и обнаружение циклических зависимостей[
dependency-cruiser](https://github.com/sverweij/dependency-cruiser) — анализ и валидация зависимостейЛинтеры — некоторые ESLint плагины могут обнаруживать циклические зависимости
Проблема автоматических исправлений
Автодополнение (автоподсказка и применяемые изменения при нажатии на кнопку Tab) и Quick Edit (вызывается правой кнопкой мыши по коду) в Cursor чаще всего предлагают поверхностное исправление. Возможно, где-то это поможет, а где-то оставит структурную проблему, но в моменте исправит.
Как cursor может помочь решить проблему?
Автоматические исправления могут предложить следующие решения:
1. Добавление type к импорту типов
Даже если между модулями есть рекурсивные импорты, в финальном бандле этот импорт удалится, т.к.
typeотносится к TypeScript (данное решение маскирует проблему)Пример:
import type { User } from './types'вместоimport { User } from './types'
Использование type в импортах может временно решить проблему, но маскирует реальную проблему организации кода.
2. Исправление импорта с barrel файла на конкретный модуль
Решает проблему, но не всегда корректно, т.к. может импортировать модуль напрямую
Некорректный пример (предложенный Cursor):
import { clientActions } from "@modules/client/redux";Корректный (вот тут, например, возникает рекурсивный импорт):
import { clientActions } from "@modules/client";3. Рефакторинг структуры кода (рекомендуемый подход)
Внести правки в организацию кода: вынести в отдельный модуль общие части из обоих модулей, либо разделить на более мелкие модули, чтобы уменьшить зацепление и таким образом разорвать рекурсивные импорты.
Пример рефакторинга:
До (проблема с рекурсивными импортами):
// @modules/client/service.ts
import { orderService } from "@modules/order";
// @modules/order/service.ts
import { clientService } from "@modules/client";
// Циклическая зависимость: client → order → clientПосле (решение через выделение общего модуля):
// @modules/shared/types.ts - общие типы
export type { Client, Order } from "./types";
// @modules/client/service.ts
import type { Client, Order } from "@modules/shared/types";
import { orderService } from "@modules/order";
// @modules/order/service.ts
import type { Client, Order } from "@modules/shared/types";
import { clientService } from "@modules/client";
// Теперь оба модуля зависят от shared, но не друг от друга напрямуюАльтернативное решение (разделение на более мелкие модули):
// Разделяем client на client-core и client-ui
// @modules/client-core/service.ts - базовая логика
// @modules/client-ui/component.tsx - UI компоненты
// @modules/order/service.ts теперь импортирует только client-core
import { clientCoreService } from "@modules/client-core";С чего начать
Если перед вами стоит задача разрешить рекурсивные импорты, это значит, что до этого в вашем проекте отсутствовали четкие правила разделения кода, либо было пренебрежение к ним.
Важно: Для того чтобы начать исправлять ошибку, необходимо пройтись по всей цепочке импортов, т.к. быстрое исправление в одном месте только маскирует проблему плохо организованного кода.
1. Анализ текущего состояния
Прежде чем приступать к исправлению, необходимо:
Обнаружение рекурсивные импортов в проекте с помощью плагина eslint-plugin-import
Определить причины возникновения циклических зависимостей
Выявить архитектурные проблемы: неправильное использование barrel файлов, нарушение принципов модульности, отсутствие четких границ между модулями
Оценить масштаб проблемы: сколько модулей затронуто, какие из них критичны
2. Разработка плана исправления
После анализа необходимо:
Выстроить план по постепенному исправлению ошибок с приоритизацией:
Начать с наиболее критичных модулей (часто используемых, влияющих на другие части системы)
Исправить простые случаи (например, неправильное использование barrel файлов)
Затем перейти к сложным рефакторингам (выделение общих модулей, разделение на более мелкие части)
Определить целевые правила организации кода (например, следование Feature Slices Design)
Документировать правила импортов и структуры модулей для команды
3. Внедрение правил в процесс разработки
Чтобы предотвратить появление новых проблем:
Написание нового кода с учетом разработанного плана и правил
Обучение команды новым правилам и принципам организации кода
Проведение код-ревью с учетом этого плана, т.к. необходимо изменить привычку писать "по-привычному"
Документирование примеров правильного и неправильного использования импортов
4. Автоматизация проверок
Настроить автоматические проверки, чтобы разработчики видели нарушения правил в IDE и не могли залить проблемный код:
Настроить правила ESLint на обнаружение рекурсивных зависимостей
Использовать
eslint-plugin-boundariesдля запрета определенных импортов между слоями/модулямиНастроить автоматический запуск ESLint при pull request, чтобы нельзя было залить изменения, если присутствуют рекурсивные импорты
Интегрировать проверки в CI/CD для автоматической валидации зависимостей
Заключение
Рекурсивные импорты — это симптом плохой организации кода, а не просто техническая проблема. Они возникают, когда отсутствуют четкие правила разделения кода или когда разработчики пренебрегают архитектурными принципами.
1. Не маскируйте проблему: Использование type импортов или поверхностные исправления в одном месте только скрывают проблему, но не решают её. Необходимо анализировать всю цепочку зависимостей.
2. Правильная организация с самого начала: Следование принципам Feature Slices Design или другим архитектурным подходам, правильное использование barrel файлов и четкое разделение модулей поможет избежать этих проблем на этапе проектирования.
3. Barrel файлы — инструмент, а не решение всех проблем: Используйте их осознанно — только внутри модулей для экспорта публичного API и только когда модуль является leaf node в графе зависимостей.
4. Автоматизация — ваш друг: Настройте ESLint правила и CI/CD проверки, чтобы предотвратить появление новых рекурсивных импортов и помочь команде следовать установленным правилам.
5. Рефакторинг требует системного подхода: При исправлении существующих проблем необходим анализ, планирование, обучение команды и постепенное внедрение изменений.
Помните: хорошо организованная архитектура модулей не только предотвращает рекурсивные импорты, но и делает код более поддерживаемым, тестируемым и понятным для всей команды.
Бонус. Работа с Cursor
Этот проект можно использовать как практическое ��адание для работы с AI-ассистентами, такими как Cursor. Вот пошаговый подход:
Шаг 1: Анализ проекта
Попросите Cursor проанализировать статью и проект, чтобы он:
Сформировал файл markdown с предполагаемыми ошибками в проекте
Предложил решения для каждой ошибки
Составил поэтапный план исправления
Шаг 2: Проверка и корректировка
После того как Cursor предложит свои выводы:
Внимательно ознакомьтесь с предложенным планом
Подкорректируйте его выводы, если необходимо
Убедитесь, что план учитывает все нюансы вашего проекта
Шаг 3: Постепенное исправление
Дайте Cursor задание исправить обнаруженные ошибки, но делайте это постепенно:
Исправляйте ошибки по одной или небольшими группами
Проверяйте, что решения не ломают функциональность
Тестируйте после каждого изменения
Такой подход поможет безопасно рефакторить код и избежать проблем в production.
