В крупных проектах относительные пути быстро становятся трудно поддерживаемыми:
import Button from '../. ./. ./. ./components/ui/Button';
import apiClient from '../. ./. ./shared/api/client';
import formatDate from '../. ./utils/date/formatDate';
При глубокой вложенности модулей возникают проблемы:
Для решения этой проблемы TypeScript поддерживает механизм алиасов
путей через compilerOptions.paths в
tsconfig.json.
Webpack, в свою очередь, не умеет автоматически читать эти настройки.
Если настроить paths только в TypeScript, приложение может
успешно компилироваться редактором и tsc, но падать во
время сборки Webpack.
Именно для синхронизации TypeScript aliases и Webpack используется
пакет tsconfig-paths-webpack-plugin.
Простейший пример:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}
Теперь вместо:
import Header from '../. ./. ./components/Header';
можно писать:
import Header from '@/components/Header';
Параметр baseUrl определяет базовую директорию,
относительно которой работают aliases.
Пример:
{
"compilerOptions": {
"baseUrl": "."
}
}
Точка означает корень проекта.
Если указать:
{
"compilerOptions": {
"baseUrl": "./src"
}
}
то абсолютные импорты будут разрешаться относительно
src.
Например:
import api from 'shared/api';
будет искать:
src/shared/api
{
"paths": {
"alias": ["path"],
"alias/*": ["path/*"]
}
}
Пример:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@components/*": ["src/components/*"],
"@pages/*": ["src/pages/*"],
"@shared/*": ["src/shared/*"]
}
}
}
Использование:
import Button from '@components/Button';
import HomePage from '@pages/HomePage';
import apiClient from '@shared/api/client';
Важно понимать: TypeScript не переписывает import paths в результирующем JavaScript.
Например:
import Button from '@components/Button';
После компиляции путь останется:
import Button from '@components/Button';
TypeScript лишь проверяет существование модуля и корректность типов.
Разрешение алиасов во время выполнения должен обеспечивать:
Частая ошибка:
{
"compilerOptions": {
"paths": {
"@/*": ["src/*"]
}
}
}
TypeScript:
import App from '@/App';
IDE не показывает ошибок.
Но Webpack выдаёт:
Module not found: Error: Can't resolve '@/App'
Причина:
Без дополнительного плагина можно вручную продублировать aliases в Webpack:
const path = require('path');
module.exports = {
resolve: {
alias: {
'@': path.resolve(__dirname, 'src')
}
}
};
Недостатки такого подхода:
Пакет автоматически читает:
baseUrl;paths;И передаёт эти настройки в систему module resolution Webpack.
npm install tsconfig-paths-webpack-plugin --save-dev
или:
yarn add tsconfig-paths-webpack-plugin -D
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@components/*": ["src/components/*"],
"@shared/*": ["src/shared/*"]
}
}
}
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
module.exports = {
resolve: {
plugins: [
new TsconfigPathsPlugin()
]
}
};
После этого Webpack начинает понимать aliases из TypeScript.
Плагин:
Находит tsconfig.json.
Читает compilerOptions.
Извлекает:
baseUrl;paths.Интегрируется в resolver Webpack.
Перехватывает module resolution.
Подменяет alias-пути на реальные файловые пути.
project/
├── src/
│ ├── components/
│ ├── pages/
│ ├── shared/
│ └── app/
├── tsconfig.json
└── webpack.config.js
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@app/*": ["src/app/*"],
"@pages/*": ["src/pages/*"],
"@components/*": ["src/components/*"],
"@shared/*": ["src/shared/*"]
}
}
}
const path = require('path');
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
module.exports = {
entry: './src/index.ts',
resolve: {
extensions: ['.ts', '.tsx', '.js'],
plugins: [
new TsconfigPathsPlugin({
configFile: path.resolve(__dirname, 'tsconfig.json')
})
]
},
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader'
}
]
}
};
import Button from '@components/Button';
import Modal from '@components/Modal';
import apiClient from '@shared/api/client';
import formatDate from '@shared/utils/date';
import HomePage from '@pages/HomePage';
import ProfilePage from '@pages/ProfilePage';
{
"paths": {
"@services/*": ["src/services/*"]
}
}
import authService from '@services/auth';
import userService from '@services/user';
Wildcard * заменяется соответствующей частью пути.
Допустим:
{
"paths": {
"@config": ["src/config/index.ts"]
}
}
Использование:
import config from '@config';
Это удобно для:
По умолчанию плагин ищет:
tsconfig.json
Но можно указать файл явно:
new TsconfigPathsPlugin({
configFile: './configs/tsconfig.frontend.json'
})
В monorepo часто используются:
packages/
apps/
shared/
И несколько tsconfig:
tsconfig.base.json
tsconfig.client.json
tsconfig.server.json
Пример:
new TsconfigPathsPlugin({
configFile: './tsconfig.client.json'
})
Наиболее распространённая связка:
TypeScript
+ ts-loader
+ tsconfig-paths-webpack-plugin
Пример:
module.exports = {
resolve: {
extensions: ['.ts', '.tsx', '.js'],
plugins: [
new TsconfigPathsPlugin()
]
},
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader'
}
]
}
};
Плагин работает независимо от transpiler.
Даже если TypeScript обрабатывается через Babel:
{
test: /\.tsx?$/,
use: {
loader: 'babel-loader'
}
}
aliases продолжат работать.
Типичная production-конфигурация:
babel-loader
+ fork-ts-checker-webpack-plugin
+ tsconfig-paths-webpack-plugin
Поскольку проверка типов выполняется отдельно, aliases остаются централизованными в tsconfig.
Плагин учитывает resolve.extensions.
Пример:
resolve: {
extensions: ['.ts', '.tsx', '.js'],
plugins: [
new TsconfigPathsPlugin()
]
}
Webpack сможет находить:
Button.ts
Button.tsx
Button.js
без указания расширения.
Иногда требуется явно передать extensions:
new TsconfigPathsPlugin({
extensions: ['.ts', '.tsx', '.js']
})
Это особенно важно при нестандартной конфигурации resolver.
Плагин интегрируется в стандартный механизм resolution Webpack.
Например:
resolve: {
mainFields: ['browser', 'module', 'main']
}
Aliases продолжают корректно работать.
Допустим:
{
"compilerOptions": {
"baseUrl": "./src"
}
}
Теперь можно писать:
import api from 'shared/api';
import Button from 'components/Button';
без ../. ./. ./.
Плагин также поддерживает этот сценарий.
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@app/*": ["src/app/*"],
"@processes/*": ["src/processes/*"],
"@pages/*": ["src/pages/*"],
"@widgets/*": ["src/widgets/*"],
"@features/*": ["src/features/*"],
"@entities/*": ["src/entities/*"],
"@shared/*": ["src/shared/*"]
}
}
}
import Header from '@widgets/Header';
import LoginForm from '@features/auth/LoginForm';
import UserCard from '@entities/user/UserCard';
Такой подход широко применяется в архитектурах:
Плагин практически не влияет на скорость сборки.
Основные затраты:
Обычно это выполняется один раз при старте сборки.
Неправильно:
plugins: [
new TsconfigPathsPlugin()
]
Правильно:
resolve: {
plugins: [
new TsconfigPathsPlugin()
]
}
Ошибка:
{
"compilerOptions": {
"paths": {
"@/*": ["src/*"]
}
}
}
Без baseUrl aliases работать не будут.
Правильно:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}
Ошибка:
{
"@components/*": ["src/components/*"]
}
Импорт:
import Button from '@/components/Button';
Aliases должны совпадать.
Если одновременно используются:
resolve.alias
и:
TsconfigPathsPlugin
возможны конфликты resolution priority.
Например:
resolve: {
alias: {
'@': path.resolve(__dirname, 'legacy')
},
plugins: [
new TsconfigPathsPlugin()
]
}
Поведение становится неоднозначным.
Лучше использовать единый источник истины.
Webpack aliases не влияют на Jest.
Для тестов требуется отдельная настройка:
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1'
}
или использование:
ts-jest
pathsToModuleNameMapper
Node.js не понимает TypeScript aliases автоматически.
Например:
node dist/index.js
может завершиться ошибкой:
Cannot find module '@shared/utils'
Возможные решения:
npm install tsconfig-paths --save-dev
ts-node -r tsconfig-paths/register src/index.ts
Теперь aliases работают и вне Webpack.
Используется:
Используется:
Webpack выполняет:
Плагин встраивается именно в этап resolver plugins.
Для диагностики можно включить logging:
infrastructureLogging: {
level: 'verbose'
}
или:
webpack --profile --progress
const path = require('path');
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
module.exports = {
mode: 'production',
entry: './src/index.tsx',
resolve: {
extensions: ['.tsx', '.ts', '.js'],
plugins: [
new TsconfigPathsPlugin({
configFile: path.resolve(__dirname, 'tsconfig.json'),
extensions: ['.tsx', '.ts', '.js']
})
]
},
module: {
rules: [
{
test: /\.tsx?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader'
}
}
]
}
};
Все aliases находятся в одном месте:
tsconfig.json
Перемещение директорий не требует массового переписывания относительных импортов.
Вместо:
../. ./. ./. ./. ./shared/lib/date
используется:
@shared/lib/date
Подход особенно полезен:
Хорошо:
@shared
@entities
@features
Плохо:
@very-long-shared-folder-name
Избыточное количество aliases усложняет навигацию.
Обычно достаточно:
@;@shared;@components;@pages;@features.Лучше придерживаться правил:
Пример:
import Button from './Button';
import Modal from '../Modal';
import api from '@shared/api';
import UserCard from '@entities/user';
Aliases становятся частью архитектурных соглашений проекта.
Например:
@shared → общие модули
@entities → бизнес-сущности
@features → пользовательские сценарии
@widgets → композиционные блоки
@pages → страницы
Это формирует предсказуемую структуру импортов и улучшает поддержку кода в долгосрочной перспективе.