Олег Марков
Настройка псевдонимов в проекте с помощью aliases-config
Введение
Настройка псевдонимов (aliases-config) помогает избавиться от длинных относительных путей вида ../../../components/Button и заменить их на понятные и стабильные конструкции вроде @components/Button или @shared/ui/Button.
Смотрите, я покажу вам, как это работает на практике. Когда в проекте растет вложенность директорий, относительные импорты начинают мешать:
// Было
import { Button } from "../../../shared/ui/Button";
// Стало
import { Button } from "@shared/ui/Button";
Псевдонимы:
- упрощают навигацию по коду;
- уменьшают количество ошибок при рефакторинге структуры папок;
- делают импорт более "семантическим" — по названию сразу видно слой или модуль.
В этой статье мы разберем, как настраивать aliases-config в типичном стекe: TypeScript / JavaScript, Webpack, Vite, Babel, ESLint, Jest. При этом вы сможете адаптировать те же принципы к любому похожему инструменту сборки.
Что такое aliases-config и зачем он нужен
Основная идея псевдонимов
Под псевдонимом понимается короткое имя, которое указывает на конкретную директорию или файл в проекте.
Простыми словами:
- без псевдонима: вы указываете путь к файлу относительно текущего файла;
- с псевдонимом: вы указываете путь относительно логического корня (alias), который однажды привязали к физическому пути.
Например, вы говорите конфигу:
// Псевдоним @shared указывает на src/shared
"@shared/*" -> "src/shared/*"
И после этого во всем проекте используете:
import { Button } from "@shared/ui/Button";
Плюсы использования псевдонимов
Давайте перечислим основные преимущества:
Читаемость
Вы сразу понимаете, к какому уровню архитектуры относится модуль:@entities,@features,@widgets,@sharedи т.д.Устойчивость к рефакторингу
Если вы переносите файл на другой уровень вложенности, абсолютные импорты с псевдонимами не ломаются, в отличие от относительных.Единый контракт для всех инструментов
Когда aliases-config настроен последовательно, TypeScript, Webpack, ESLint, тестовый раннер — все понимают одни и те же пути.Ускорение навигации в IDE
IDE проще "прыгать" по коду, а вам проще сразу понять, где физически лежит модуль по его псевдониму.
Общие принципы настройки aliases-config
Концепция "единого источника правды"
Основная проблема при настройке псевдонимов — рассинхронизация:
- TypeScript думает, что
@sharedуказывает на одну папку, - Webpack — на другую,
- Jest — не понимает этот путь вообще.
Хорошая практика — выбрать один главный конфиг (обычно tsconfig.json или отдельный aliases.config.mjs) и от него "кормить" остальные инструменты.
Чаще всего:
- для TypeScript корневым считается
tsconfig.json; - для JavaScript без TypeScript корнем становится файл сборщика (например,
vite.config.tsилиwebpack.config.js).
Базовая архитектура alias-ов
Разберем типичный пример структуры проекта:
project/
src/
app/
pages/
widgets/
features/
entities/
shared/
tsconfig.json
webpack.config.js
jest.config.cjs
.eslintrc.cjs
Здесь удобно завести такие псевдонимы:
@app→src/app@pages→src/pages@widgets→src/widgets@features→src/features@entities→src/entities@shared→src/shared
Настройка aliases-config в TypeScript
Базовый пример tsconfig.json
Давайте разберемся на примере типового tsconfig.json:
{
"compilerOptions": {
// Указываем базовую директорию для относительных путей
"baseUrl": "./src",
// Описываем псевдонимы
"paths": {
"@app/*": ["app/*"],
"@pages/*": ["pages/*"],
"@widgets/*": ["widgets/*"],
"@features/*": ["features/*"],
"@entities/*": ["entities/*"],
"@shared/*": ["shared/*"]
}
},
"include": [
"src"
]
}
Комментарии к примеру:
"baseUrl": "./src"— теперь все пути вpathsсчитаются относительноsrc."@shared/*": ["shared/*"]— эта запись говорит компилятору, что импорт@shared/ui/Buttonнужно искать по путиsrc/shared/ui/Button.
Теперь вы увидите, как это выглядит в коде:
// src/app/index.ts
// Импорт компонента c использованием псевдонима
import { Button } from "@shared/ui/Button";
// Импорт модуля из слоя features
import { LoginForm } from "@features/auth/login-form";
Особенности сборки TypeScript без бандлера
Если вы компилируете TypeScript в JavaScript без Webpack/Vite (например, через tsc и запускаете Node.js), простая настройка paths недостаточна. TypeScript умеет "понимать" псевдонимы при компиляции типов, но Node.js по умолчанию о них не знает.
Есть два распространенных варианта решения:
- Преобразовывать алиасы на этапе сборки с помощью Babel / ts-node / ts-node-dev с плагинами;
- Использовать дополнительный пакет для маппинга alias-ов в рантайме (например,
module-alias).
Здесь мы не будем углубляться в Node-only конфигурации, но важно помнить: одного tsconfig.json для рантайма недостаточно.
Настройка псевдонимов в Webpack
Пример webpack.config.js
Теперь давайте перейдем к настройке сборки. Webpack использует раздел resolve.alias:
// webpack.config.js
const path = require("path");
module.exports = {
// Остальной конфиг опущен для краткости
resolve: {
// Расширения, которые будет подставлять Webpack при импорте без указания расширения
extensions: [".ts", ".tsx", ".js", ".jsx"],
// Здесь мы задаем псевдонимы
alias: {
// Псевдоним @app указывает на папку src/app
"@app": path.resolve(__dirname, "src/app"),
"@pages": path.resolve(__dirname, "src/pages"),
"@widgets": path.resolve(__dirname, "src/widgets"),
"@features": path.resolve(__dirname, "src/features"),
"@entities": path.resolve(__dirname, "src/entities"),
"@shared": path.resolve(__dirname, "src/shared")
}
}
};
Как видите, этот код выполняет ровно ту же задачу, что paths в TypeScript, только уже на уровне сборки.
Синхронизация Webpack с tsconfig.json
Чтобы не дублировать пути руками, многие делают так:
- Описывают alias-ы в
tsconfig.json. - Подтягивают их в Webpack автоматически через пакет
tsconfig-paths-webpack-plugin.
Пример:
// webpack.config.js
const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin");
module.exports = {
resolve: {
extensions: [".ts", ".tsx", ".js", ".jsx"],
plugins: [
// Плагин читает tsconfig.json и настраивает alias-ы автоматически
new TsconfigPathsPlugin({
configFile: "./tsconfig.json"
})
]
}
};
Комментарии:
- Плагин ищет поле
compilerOptions.pathsи превращает их в alias-ы Webpack. - Это уменьшает шанс, что вы забудете обновить Webpack после изменения alias-а.
Настройка псевдонимов в Vite
Простой пример vite.config.ts
В Vite настройка делается через resolve.alias (очень похоже на Webpack).
Давайте посмотрим, что происходит в следующем примере:
// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import path from "path";
export default defineConfig({
plugins: [
// Подключаем плагин React (если используется React)
react()
],
resolve: {
alias: {
// Псевдоним @app -> src/app
"@app": path.resolve(__dirname, "src/app"),
"@pages": path.resolve(__dirname, "src/pages"),
"@widgets": path.resolve(__dirname, "src/widgets"),
"@features": path.resolve(__dirname, "src/features"),
"@entities": path.resolve(__dirname, "src/entities"),
"@shared": path.resolve(__dirname, "src/shared")
}
}
});
Vite также поддерживает использование плагинов для считывания alias-ов из tsconfig.json, например vite-tsconfig-paths:
// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import tsconfigPaths from "vite-tsconfig-paths";
export default defineConfig({
plugins: [
react(),
// Плагин автоматически подтягивает alias-ы из tsconfig.json
tsconfigPaths()
]
});
В этом случае вам не нужно дублировать конфигурацию: вы один раз настраиваете paths в TypeScript, и Vite сам подстраивается под них.
Настройка псевдонимов в Babel
Использование плагина module-resolver
Если вы используете Babel (например, в старых проектах или вместе с Jest), удобно подключить плагин babel-plugin-module-resolver.
Пример конфигурации .babelrc:
{
"presets": [
"@babel/preset-env",
"@babel/preset-react",
"@babel/preset-typescript"
],
"plugins": [
[
"module-resolver",
{
// Указываем корень проекта
"root": ["./src"],
// Описываем alias-ы
"alias": {
"@app": "./src/app",
"@pages": "./src/pages",
"@widgets": "./src/widgets",
"@features": "./src/features",
"@entities": "./src/entities",
"@shared": "./src/shared"
}
}
]
]
}
Обратите внимание, как этот фрагмент кода решает задачу:
- Babel во время трансформации кода заменит импорты вида
@shared/ui/Buttonна реальные относительные пути; - это особенно полезно, если Jest использует Babel для трансформации тестов.
ESLint и псевдонимы
Проблема "import/no-unresolved"
Частая ситуация: вы настроили alias-ы в TypeScript и Webpack, а ESLint продолжает ругаться на импорты:
import { Button } from "@shared/ui/Button";
// ESLint: Unable to resolve path to module '@shared/ui/Button'
Чтобы исправить это, нужно объяснить ESLint, как резолвить модули. Для этого используется плагин eslint-import-resolver-typescript или eslint-import-resolver-alias.
Пример конфигурации c eslint-import-resolver-typescript
Смотрите, я покажу вам, как это работает с TypeScript:
// .eslintrc.cjs
module.exports = {
parser: "@typescript-eslint/parser",
plugins: ["@typescript-eslint", "import"],
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:import/recommended",
"plugin:import/typescript"
],
settings: {
"import/resolver": {
// Резолвер для TypeScript
typescript: {
// Указываем путь к tsconfig, откуда брать paths
project: "./tsconfig.json"
}
}
}
};
Комментарии:
- Резолвер читает
pathsизtsconfig.jsonи использует их для проверки импортов. - Важно, чтобы
projectуказывал на тот жеtsconfig.json, где настроены alias-ы.
Альтернатива: eslint-import-resolver-alias
Если у вас чистый JS-проект без TypeScript, можно использовать eslint-import-resolver-alias:
// .eslintrc.cjs
module.exports = {
settings: {
"import/resolver": {
alias: {
// Описываем псевдонимы
map: [
["@app", "./src/app"],
["@pages", "./src/pages"],
["@widgets", "./src/widgets"],
["@features", "./src/features"],
["@entities", "./src/entities"],
["@shared", "./src/shared"]
],
// Расширения файлов
extensions: [".js", ".jsx", ".ts", ".tsx"]
}
}
}
};
Настройка alias-ов в Jest
Проблема с тестами и псевдонимами
При запуске тестов через Jest импорты с псевдонимами часто не работают "из коробки". Jest не знает о ваших alias-ах и поэтому выдает ошибку Cannot find module '@shared/ui/Button'.
Чтобы это поправить, нужно использовать опцию moduleNameMapper.
Пример jest.config.cjs
Покажу вам, как это реализовано на практике:
// jest.config.cjs
/** @type {import('jest').Config} */
module.exports = {
// Указываем, какие файлы считаем модулями
moduleFileExtensions: ["js", "jsx", "ts", "tsx", "json"],
// Используем ts-jest или babel-jest для трансформации
transform: {
// Здесь обрабатываем TypeScript файлы
"^.+\\.tsx?$": "ts-jest"
},
moduleNameMapper: {
// Псевдонимы должны совпадать с вашими alias-ами в tsconfig / webpack / vite
"^@app/(.*)$": "<rootDir>/src/app/$1",
"^@pages/(.*)$": "<rootDir>/src/pages/$1",
"^@widgets/(.*)$": "<rootDir>/src/widgets/$1",
"^@features/(.*)$": "<rootDir>/src/features/$1",
"^@entities/(.*)$": "<rootDir>/src/entities/$1",
"^@shared/(.*)$": "<rootDir>/src/shared/$1"
}
};
Обратите внимание:
- Здесь используются регулярные выражения
^@shared/(.*)$, чтобы подставить хвост пути$1. <rootDir>— корень проекта (обычно папка, где лежит самjest.config.cjs).
Единый aliases-config как отдельный модуль
Иногда удобно вынести alias-ы в отдельный модуль и переиспользовать его в разных конфигурациях. Например, создать файл aliases.config.mjs, в котором вы опишете все маппинги, а затем будете подключать его в Webpack, Jest, ESLint и других местах.
Пример aliases.config.mjs
Давайте посмотрим, как это может выглядеть:
// aliases.config.mjs
import path from "path";
import { fileURLToPath } from "url";
// Здесь мы получаем __dirname в ESM окружении
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Базовая директория src
const srcPath = path.resolve(__dirname, "src");
// Общий объект с alias-ами
export const aliases = {
"@app": path.join(srcPath, "app"),
"@pages": path.join(srcPath, "pages"),
"@widgets": path.join(srcPath, "widgets"),
"@features": path.join(srcPath, "features"),
"@entities": path.join(srcPath, "entities"),
"@shared": path.join(srcPath, "shared")
};
// Вспомогательная функция для Jest (возвращает объект с паттернами)
export const jestModuleNameMapper = Object.fromEntries(
Object.entries(aliases).map(([key, value]) => {
// Преобразуем @shared -> ^@shared/(.*)$ : <rootDir>/src/shared/$1
const pattern = `^${key}/(.*)$`;
const target = `<rootDir>/${path.relative(__dirname, value)}/$1`;
return [pattern, target];
})
);
Теперь вы можете переиспользовать эту конфигурацию.
Использование aliases.config.mjs в Webpack
// webpack.config.cjs
const path = require("path");
const { aliases } = require("./aliases.config.cjs");
// Если у вас CommonJS, создайте cjs-версию или используйте динамический import
module.exports = {
resolve: {
extensions: [".ts", ".tsx", ".js", ".jsx"],
alias: {
// Здесь удобно использовать уже готовый объект
...aliases
}
}
};
Использование в Jest
// jest.config.cjs
const { jestModuleNameMapper } = require("./aliases.config.cjs");
/** @type {import('jest').Config} */
module.exports = {
transform: {
"^.+\\.tsx?$": "ts-jest"
},
moduleNameMapper: {
// Расширяем mapper из общего конфига
...jestModuleNameMapper
}
};
Такой подход уменьшает вероятность ошибок: вы меняете псевдоним в одном месте, и все инструменты автоматически получают обновление.
Практические рекомендации по выбору псевдонимов
Используйте устойчивые именования
Частая ошибка — завязываться на конкретную структуру директорий, которая быстро меняется. Например, вводить псевдонимы вида @components, @containers, @hooks, когда вы еще не уверены в архитектуре.
Более устойчивый подход:
- основывать псевдонимы на архитектурных слоях (
@shared,@entities,@features,@widgets,@pages,@app); - или на крупных доменных областях (
@auth,@profile,@orders) — если слойная архитектура еще не определена.
Не смешивайте относительные пути и alias-ы хаотично
Рекомендуемый подход:
- для внутримодульных импортов (в пределах одной "маленькой" директории) — использовать относительные пути (
./Button,../model/hooks); - для межслойных и межмодульных импортов — использовать псевдонимы (
@shared/ui/Button,@features/auth/login-form).
Так вы избегаете ситуации, когда всё в проекте импортируется только через alias-ы, и при этом у вас появляются "циклы" или слишком сильное сцепление.
Следите за циклами импортов
Псевдонимы облегчают код, но одновременно маскируют циклические зависимости. Важно:
- настроить ESLint-правила (
import/no-cycle); - либо использовать дополнительные утилиты для поиска циклов.
Когда импорты выглядят как @features/a/... и @features/b/..., циклы трудно заметить визуально, поэтому автоматическая проверка особенно полезна.
Типичные ошибки при настройке aliases-config
Ошибка 1. Псевдоним есть в tsconfig, но нет в сборщике
Симптомы:
- IDE видит модуль и подсвечивает все корректно;
- но при запуске сборки Webpack или Vite вы получаете ошибку
Module not found.
Причина:
- alias настроен в
tsconfig.json, но не настроен в Webpack/Vite.
Решение:
- Добавить соответствующий alias в
resolve.aliasWebpack/Vite; - Или подключить плагин, который автоматически читает
tsconfig.json.
Ошибка 2. Jest не понимает alias-ы
Симптомы:
- приложение собирается, но тесты падают с ошибкой
Cannot find module.
Причина:
- не настроен
moduleNameMapperв Jest.
Решение:
- добавить регулярные выражения для alias-ов в
jest.config.
Ошибка 3. ESLint ругается на импорты с alias-ами
Симптомы:
- в IDE на каждом импорте с псевдонимом висят предупреждения
import/no-unresolved.
Причина:
- ESLint использует дефолтный резолвер, который не знает о
tsconfig.pathsили других настройках.
Решение:
- настроить
eslint-import-resolver-typescriptилиeslint-import-resolver-aliasвsettings.import/resolver.
Ошибка 4. Неправильный baseUrl в tsconfig.json
Симптомы:
- TypeScript не может найти модуль, даже если alias настроен.
Причина:
baseUrlуказывает не на тот каталог, относительно которого вы определяете alias-ы.
Решение:
- проверить, что
baseUrlсоответствует корню исходников (обычно"./src"); - убедиться, что
pathsнаписаны относительноbaseUrl.
Теперь давайте подведем итог.
Псевдонимы (aliases-config) — это способ дать более понятные и устойчивые имена путям в вашем проекте. Ключевые моменты:
- описывайте alias-ы однократно и дальше старайтесь переиспользовать их во всех инструментах;
- синхронизируйте настройки TypeScript, сборщика, тестов и линтера;
- выбирайте псевдонимы, основанные на архитектурных слоях или доменных областях;
- контролируйте качество импортов через ESLint и тесты.
Если вы выстроите единый aliases-config, дальнейшая поддержка и развитие проекта станет заметно проще.
Частозадаваемые технические вопросы
Как настроить псевдонимы, если у меня монорепозиторий с несколькими пакетами?
В монорепо (например, с Yarn Workspaces или PNPM) удобно организовать alias-ы так:
- В корне каждому пакету дать "имя" через
package.json("name": "@my-org/ui"). - Использовать это имя как импорт:
import { Button } from "@my-org/ui";. - Для локальной разработки:
- настроить
pathsв корневомtsconfig.json, например"@my-org/ui": ["packages/ui/src"]; - в сборщике каждого приложения добавить alias
@my-org/ui→../ui/srcили использовать сборку ui-пакета отдельно.
- настроить
Такой подход позволяет использовать один и тот же импорт и локально, и после публикации пакета.
Как сделать, чтобы IDE (VS Code) корректно переходила по alias-ам?
Для VS Code важно:
- Чтобы в проекте был корректно настроен
tsconfig.jsonсbaseUrlиpaths. - Чтобы вы открывали именно корневую папку проекта, где лежит
tsconfig.json. - Если вы используете несколько tsconfig (например,
tsconfig.base.jsonиtsconfig.app.json), убедитесь, что:- IDE знает, какой из них основной (обычно это
tsconfig.jsonв корне); - alias-ы описаны в базовом или общем конфиге, который расширяется в остальных.
- IDE знает, какой из них основной (обычно это
Как поступать с alias-ами при сборке библиотеки (npm-пакет)?
Для библиотек удобнее:
- Внутри библиотеки использовать относительные пути или alias-ы, которые потом "разворачиваются" до относительных при сборке.
- Не оставлять в собранном пакете alias-импорты (
@shared/...), если вы не публикуете вместе с ним и конфигурацию сборки/рантайма. - В практической схеме:
- Настроить alias-ы в исходниках.
- Собирать библиотеку через Rollup/Webpack/Vite, которые раскроют alias-ы до относительных путей.
- Публиковать уже собранный код.
Можно ли использовать один и тот же alias для разных платформ (web, node, mobile) с разными реализациями?
Да, это делается через "conditional exports" или разное разрешение модулей:
- В Node.js можно использовать поле
exportsвpackage.json, указывая разные файлы для разных сред. - В бандлерах можно настроить
resolve.aliasтак, чтобы:- для web-версии
@platformуказывал наsrc/platform/web; - для node-версии — на
src/platform/node.
- для web-версии
Главное — не путать эти конфигурации и четко понимать, где какой конфиг используется.
Как временно отключить alias-ы для отладки конкретной проблемы?
Самый простой подход:
- Временно заменить импорт с alias-а на относительный путь там, где вы отлаживаете проблему.
- Если хотите изменить поведение глобально:
- закомментировать соответствующие alias-ы в конфиге сборщика;
- запустить сборку и посмотреть, какие модули теперь не находятся;
- после отладки вернуть конфигурацию обратно.
Важно не забыть вернуть alias-ы и не закоммитить временные изменения в основную ветку.