28 апр. 2026 г.·7 мин чтения

Алиасы путей TypeScript и barrel-файлы в больших репозиториях

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

Алиасы путей TypeScript и barrel-файлы в больших репозиториях

Почему в больших репозиториях импорты становятся трудными для понимания

Небольшой фронтенд-репозиторий обычно начинается понятно. Вы открываете файл, видите ../Button или ../../utils/date и всё ещё можете представить, где живёт этот код. Через год в том же приложении уже десятки папок, общие пакеты, feature-модули и старые пути, которые никто не привёл в порядок. Тогда импорты перестают быть подсказками и начинают больше походить на догадки.

Сначала проблему создают длинные относительные пути. Когда файл импортирует ../../../../components/Button, понятно, что код где-то далеко, но не больше. Это кнопка от команды checkout, от design system или из старой общей папки, к которой никто не хочет прикасаться? Часто нужны ещё три клика только для того, чтобы понять, куда именно указывает импорт.

Поиск по коду начинает тормозить уже по другой причине. В больших репозиториях часто много файлов, которые экспортируют одно и то же имя: Button, Modal, useAuth, formatDate, index.ts. Если искать export const Button, можно получить десять совпадений. Если искать import { Button }, можно получить сотни. Строка импорта выглядит аккуратно, но перестаёт говорить, с каким именно Button вы работаете.

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

Именно поэтому команды вообще смотрят на такие приёмы, как алиасы путей TypeScript и barrel-файлы. Им нужны импорты, которые быстрее писать и проще просматривать. Это понятная цель. Но есть подвох: более аккуратные импорты на поверхности могут сделать репозиторий труднее для чтения, если при этом скрывают, где код действительно живёт.

Хороший стиль импортов делает сразу две вещи: убирает путаницу в путях и при этом оставляет понятные подсказки о местоположении и владении. Если он делает только первое, репозиторий кажется аккуратным ровно до тех пор, пока кому-то не нужно его дебажить.

Что реально решают алиасы путей

Алиасы путей решают простую проблему: импорты, которые ползут вверх и вниз по дереву с помощью ../../../../. Относительные пути по-прежнему работают, но читать их тяжело. Когда приходится считать точки, прежде чем увидеть реальную зависимость, строка импорта перестаёт быть полезной.

Алиас путей превращает это во что-то вроде @ui/Button или @shared/date. Такое небольшое изменение делает файлы легче для чтения. Можно посмотреть на пять импортов и быстро понять, зависит ли файл от общего кода, кода фичи или кода на уровне приложения.

Алиасы также уменьшают шум во время рефакторинга. Если команда переносит компонент из одной папки в другую, глубокие относительные импорты часто ломаются повсюду. С алиасами импорт может остаться прежним даже тогда, когда файл переезжает внутри области, на которую указывает mapping.

Где алиасы помогают, а где нет

Алиасы путей TypeScript лучше всего работают как ярлыки к папкам. Они дают разработчикам более чистый способ указывать на части репозитория, которыми они пользуются часто. В большом фронтенд-репозитории это экономит время каждый день.

Но ярлык на папку — это не то же самое, что публичная граница модуля. @shared/* может означать только одно: "у этой папки есть алиас". Он не говорит, какие файлы внутри должны использовать другие команды, кто за них отвечает и какие импорты безопасны в долгую. Для этого всё ещё нужна структура и правила.

Более короткие импорты могут даже скрывать неаккуратную организацию. @components/Button выглядит опрятно, но может указывать на папку, где рядом лежат десятки несвязанных файлов. Путь короче, а владение всё равно размыто. Аккуратная строка импорта не гарантирует аккуратную архитектуру.

Хорошая проверка простая. Если алиас помогает человеку догадаться, где живёт код и почему этот файл от него зависит, он работает. Если он просто скрывает расстояние в дереве папок, он улучшает набор текста, а не структуру.

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

Почему командам нравятся barrel-файлы

Barrel-файл — это обычно файл index.ts, который переэкспортирует код из соседних файлов. Вместо того чтобы импортировать каждый компонент по отдельному пути, вы импортируете всё из одной точки входа папки.

// ui/index.ts
export { Button } from './Button'
export { Modal } from './Modal'
export { Tooltip } from './Tooltip'

// somewhere else
import { Button, Modal, Tooltip } from '@/ui'

Такое небольшое изменение приятно в загруженном репозитории. Строки импортов становятся короче, diff выглядит аккуратнее, и разработчики меньше времени тратят на просмотр длинных путей. Когда файл импортирует шесть UI-элементов, один сгруппированный импорт воспринимается легче, чем шесть отдельных строк.

Команды также любят barrel-файлы, потому что папки начинают ощущаться как модули, а не как куча файлов. Файл components/forms/index.ts делает папке одну публичную поверхность. После этого проще говорить: "используйте то, что экспортирует папка", вместо того чтобы заставлять всех помнить точные имена файлов внутри.

Особенно это полезно в общих папках. Design system, общий пакет хуков или каталог ui обычно быстро разрастаются. Новые люди приходят, копируют существующий импорт и идут дальше. Им не нужно знать, лежит ли Button в Button.tsx, button/Button.tsx или primitives/Button.tsx. Barrel-файл скрывает эту деталь.

UI-библиотеки — как раз тот случай, где сгруппированные экспорты выглядят естественно. Если команда использует Button, Card, Input и Modal на многих экранах, одна строка импорта делает интерфейсы чище. Алиасы путей TypeScript делают это ещё привлекательнее, потому что @/ui выглядит аккуратнее, чем цепочка относительных путей.

Есть и практическая причина, по которой команды рано добавляют index.ts: рефакторить становится проще. Можно переносить файлы внутри папки, сохранять те же экспорты и не трогать сразу каждый consumer. Для фронтенд-репозитория, который меняется каждую неделю, это реальное удобство.

Итак, привлекательность проста. Barrel-файлы убирают шум, дают папке публичную точку входа и делают частые импорты предсказуемыми.

Где эти приёмы начинают скрывать владение

Алиасы путей TypeScript могут привести в порядок некрасивые относительные импорты. Проблема начинается тогда, когда путь импорта перестаёт подсказывать, где код живёт. Если файл импортирует Button из @ui/Button, вы можете не понять, находится ли этот код в том же приложении, в общем пакете или в папке, которой владеет другая команда.

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

Barrel-файлы добавляют ещё один слой тумана. Когда папка переэкспортирует десять компонентов через index.ts, строка импорта выглядит аккуратно, но реальный владелец исчезает. Вы видите @ui или components, а не файл, пакет или команду, которые поддерживают этот код.

Проблема становится хуже, когда во многих папках повторяются одни и те же имена. В больших репозиториях часто оказывается несколько модулей Button, Modal или utils. Ищешь Button — и видишь страницы barrel-экспортов, прежде чем находишь тот самый компонент, который рендерится на странице.

Редактор тоже может замедлять работу. Команда "перейти к определению" часто сначала попадает на barrel-файл, потом на второй barrel-файл и только затем — на исходник. Текстовый поиск делает то же самое: находит переэкспорты, а не место, где человек действительно написал код.

Это влияет и на ревью. Когда в pull request меняются импорты из @shared или @core, ревьюерам нужны дополнительные клики, чтобы понять, кто владеет кодом и переходит ли изменение границу команды. Переименование или удаление модуля тоже становится сложнее по той же причине: граф импортов выглядит более плоским, чем есть на самом деле.

Небольшой пример показывает компромисс. import { Button } from "@shared/ui" выглядит чисто. Но import { Button } from "../../checkout/components/Button" говорит намного больше: код локален для checkout, и, скорее всего, им владеет команда checkout.

Аккуратные импорты — это приятно. Понятное владение — важнее. Если алиасы и barrel-файлы стирают разницу между локальным кодом, общим кодом и кодом другой команды, они делают ревью, рефакторинг и отладку медленнее, чем те неуклюжие импорты, которые они заменили.

Как выбирать подход шаг за шагом

Упростите общий UI
Посмотрите, остаются ли экспортные точки дизайн-системы понятными по мере роста репозитория.

Начните с обычных относительных импортов и какое-то время оставьте их в покое. Они некрасивые, но честные. Когда файл говорит ../../checkout/api, обычно можно понять, где он лежит и кто за него отвечает.

Потом ищите повторяющуюся боль, а не жалобы на стиль. Если люди постоянно поднимаются по одним и тем же большим папкам или перенос общей области ломает множество импортов, это уже реальный сигнал. В растущем репозитории обычно есть несколько папок, которые почти не двигаются: например, app, features или shared.

Вот здесь алиасы путей TypeScript и помогают. Ограничьте их этими стабильными верхнеуровневыми областями. Небольшого набора вроде @app, @features и @shared часто достаточно. Если создавать алиасы для каждой подпапки, вы потеряете карту репозитория, а импорты начнут выглядеть одинаково расплывчато.

С barrel-файлами нужна ещё большая сдержанность. Используйте их там, где хотите иметь понятную публичную точку входа для других частей кодовой базы. Если фича profile должна экспортировать один хук, один компонент и один тип, index.ts может сделать этот контракт очевидным.

Внутри фичи лучше обойтись без barrel-файла и импортировать реальный файл. Прямые пути помогают легче проследить владение. Они также делают внутренние рефакторинги локальными, вместо того чтобы превращать один index.ts в переполненный хаб экспортов.

Обычно хорошо живёт такой набор правил:

  • алиасы — для нескольких верхнеуровневых папок, которые почти не двигаются
  • barrel-файлы — только на границах фич или пакетов
  • прямые импорты — для файлов внутри одной фичи
  • без barrel-файлов — для помощников, тестов и приватных подкомпонентов

Перед тем как стандартизировать что-либо, проверьте это одним простым тестом. Попросите нового участника команды проследить три импорта до исходника. Если он быстро находит файл, ваши правила работают. Если он прыгает через несколько index.ts или вынужден пользоваться поиском по всему репозиторию, значит shortcuts скрывают владение вместо того, чтобы помогать.

Этот последний тест важнее красивых строк импортов. Аккуратные импорты — это приятно. Быстрый поиск по коду — лучше.

Простой пример из растущего фронтенд-репозитория

Представьте репозиторий, который сначала был одним приложением, а потом со временем оброс общим кодом:

src/
  app/
  shared/
  design-system/
  features/

Через несколько месяцев импорты начинают растягиваться через всё дерево. Страница профиля внутри app может подтянуть helper для дат вот так:

import { formatDate } from "../../../../shared/date/formatDate";

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

import { formatDate } from "@shared/date/formatDate";

Это сразу читается лучше. И при этом остаётся видно, что код приходит из shared, а не из локальной фичи. Если кто-то ищет @shared/date/formatDate, он получает чистый список мест, где этот код используется.

Barrel-файлы могут работать так же, когда папка действительно ведёт себя как пакет. Design system — хороший пример. Если design-system/index.ts экспортирует Button, Input и Modal, этот импорт легко читать:

import { Button, Input } from "@design-system";

Такой shortcut обычно работает, потому что у пакета компонентов есть понятная публичная поверхность. Поиск тоже остаётся полезным: поиск по @design-system показывает, какие части приложения зависят от пакета, а поиск внутри barrel-файла показывает, что именно пакет экспортирует.

Проблема начинается тогда, когда barrel-файл фичи становится слишком широким. Допустим, features/checkout/index.ts экспортирует UI-части, API-вызовы, состояние, хуки и утилиты сразу из одного места:

import { CartDrawer, submitOrder, useCart } from "@features/checkout";

Теперь владение размывается. submitOrder звучит как API-код, CartDrawer — как UI, а useCart — как логика состояния, но импорт скрывает это разделение. Поиск по @features/checkout возвращает кучу файлов и не говорит, какая из них относится к какому слою. Поиск по submitOrder часто сначала попадает на barrel-файл, а потом заставляет сделать ещё один прыжок, прежде чем вы найдёте настоящий модуль.

До появления такого barrel-файла поиск по @features/checkout/api/submitOrder или по прямому пути к файлу даёт более точный результат. После — поиск становится шире, и люди тратят больше времени на клики, пытаясь понять, кто чем владеет.

Ошибки, из-за которых поиск по коду становится хуже

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

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

Одна из частых ошибок — добавить алиасы путей TypeScript для каждой папки. Несколько алиасов для стабильных, общих областей помогают. Двадцать алиасов превращают репозиторий в карту, которую помнит только исходная команда. Когда одновременно существуют @ui, @shared, @common, @base и @components, результаты поиска перестают быть понятными, потому что похожий код живёт сразу под несколькими схемами именования.

Barrel-файлы создают другую проблему. Они хороши, когда раскрывают небольшой публичный API модуля. Они становятся вредными, когда верхнеуровневый barrel переэкспортирует глубокие внутренности из множества мест. Тогда поиск по импорту показывает @app/ui вместо реального файла, и никто не может быстро сказать, относится ли компонент к checkout, design system или какому-то старому feature-слою.

Повторяющиеся имена быстро усугубляют ситуацию. Если три несвязанных модуля экспортируют Button, Modal или Header, поиск превращается в угадайку. Разработчики открывают не тот файл, меняют не тот стиль, а потом удивляются, почему сломался другой экран.

Обычно к большей части боли приводят несколько шаблонов:

  • один index.ts экспортирует и публичные элементы API, и приватные помощники
  • глубокие файлы переэкспортируются через два или три слоя barrel-файлов
  • команды импортируют один и тот же модуль через относительный путь, алиас и barrel-путь
  • алиасы папок слишком близко повторяют физическую структуру и со временем размножаются
  • слишком общие названия компонентов повторяются в несвязанных модулях

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

Разъезд стиля импортов — самая медленная, но и самая липкая проблема. Если одна папка использует относительные импорты, другая — алиасы, а третья идёт через barrel-файлы, поиск по коду даёт три разных ответа на один и тот же вопрос. Выбирайте правило, которое соответствует владению. У общих модулей может быть публичный путь импорта. Локальный код обычно должен оставаться локальным и импортироваться через относительный путь.

Если путь импорта скрывает владельца, скорее всего, он слишком умный.

Короткие проверки перед стандартизацией импортов

Проверьте правила импортов
Попросите Oleg помочь найти алиасы и barrel-файлы, которые размывают владение модулем.

Репозиторий обычно быстро показывает, выдержат ли его правила импортов проверку временем. Если разработчик открывает файл, видит импорт и всё равно должен пройти через три слоя, чтобы найти исходник, правило слишком умное.

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

Перед тем как зафиксировать один стиль для всей кодовой базы, используйте короткую проверку:

  • Возьмите пять распространённых импортов из разных областей. Если за несколько секунд нельзя понять, кто владеет каждым модулем, именование скрывает слишком много. @ui, @shared и @core часто звучат аккуратно, но быстро становятся расплывчатыми.
  • Запустите поиск по импортируемому символу. Вам нужен один очевидный исходный файл, а не десять переэкспортов через barrel-файлы. Если поиск сначала попадает на index-файлы, а настоящий код кажется спрятанным, поиск по коду и дальше будет медленным.
  • Проверьте, живут ли публичные экспорты и внутренние помощники в разных местах. Barrel-файл должен показывать то, что могут использовать другие команды. Он не должен тихо протаскивать приватные утилиты, тестовые данные или недоделанные компоненты.
  • Добавьте один новый файл так, как будто вы пришли в команду вчера. Если приходится гадать, он должен лежать в @components, @shared/components или в локальном barrel-файле, правило недостаточно ясно.
  • Сравните алиасы с реальной архитектурой, а не с модой на структуру папок. Хорошие алиасы указывают на стабильные границы: продуктовые области, пакеты design system или слои приложения. Плохие алиасы просто переименовывают глубокие папки и делают вид, что структура стала лучше.

Есть и маленький тревожный знак: когда импорты читаются красиво, но ничего не говорят. import { formatPrice } from "@shared" выглядит опрятно. import { formatPrice } from "@billing/formatters" длиннее, но обычно команды работают быстрее со вторым вариантом, потому что владелец очевиден.

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

Что делать дальше в своём репозитории

Начните с короткого командного правила, которое помещается на полстраницы. Если для объяснения импортов нужна отдельная встреча, правило слишком длинное. Хорошее правило говорит, где разрешены алиасы, когда разрешён barrel-файл и когда инженеры обязаны импортировать из реального пути к модулю.

Держите алиасы немногочисленными и скучными. Один алиас для общего UI, один для кода приложения и один для тестовых помощников часто уже достаточно. Пересматривайте алиасы путей TypeScript два раза в год. Если алиас скрывает, где живёт код, или постепенно разрастается до несвязанных папок, убирайте его.

С barrel-файлами нужен ещё более строгий фильтр. Оставляйте их там, где они показывают стабильный публичный API папки, которой пользуется много людей. Удаляйте их, когда они экономят всего несколько символов или когда заставляют поиск прыгать через три файла, прежде чем вы найдёте владельца. В большом репозитории такая мелочь отнимает время каждую неделю.

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

Простая политика может выглядеть так:

  • Используйте прямые импорты внутри одной области фичи.
  • Используйте алиасы только для стабильных верхнеуровневых областей.
  • Оставляйте barrel-файлы только там, где у папки есть понятный публичный API.
  • Помечайте общие модули владельцем и правилом ревью.

Если репозиторий уже кажется грязным, не пытайтесь исправить сразу каждый импорт. Начните с папок, которые меняются каждую неделю и чаще всего создают путаницу в ревью. Сначала наведите порядок там, закрепите правило в code review, а тихие углы оставьте на потом.

Некоторые команды могут справиться с такой чисткой сами. Другим нужен внешний взгляд, потому что старые shortcuts завязаны на привычки команды, настройки сборки и пробелы во владении. В таком случае Fractional CTO или советник вроде Oleg может посмотреть на структуру репозитория, найти места, где импорты скрывают владение модулем, и помочь установить правила, которым люди действительно будут следовать. Даже короткий разбор может остановить месяцы тихой, но постоянной путаницы.