Пути в tsconfig.json: что скрывается под капотом TypeScript

Онлайн-тренажер Python для начинающих

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

Начать курс

Как алиасы в TypeScript экономят часы вашей жизни

Вы когда-нибудь писали что-то вроде import UserService from '../../../../../services/UserService'? Если да — вы знаете эту боль. Глаза разбегаются, рефакторинг превращается в кошмар, а любой перенос файла ломает всю цепочку импортов. К счастью, TypeScript (и его конфигурационный файл tsconfig.json) предлагает элегантное решение — paths.

Но вот вопрос: как это работает под капотом? Почему, прописав алиас "@/*", вы можете писать import { UserService } from '@/services/UserService'? И главное — почему это иногда не работает без дополнительных настроек сборщика (webpack, Vite, Rollup)?

В этой статье мы не просто покажем, как настроить paths в tsconfig.json. Мы заглянем внутрь механизма, разберёмся с path mapping, узнаем, как TypeScript резолвит модули, и научимся избегать типичных ошибок.

Что такое paths в tsconfig.json?

Paths — это секция в tsconfig.json, которая позволяет задать пользовательские сопоставления (mapping) между псевдонимами импортов и реальными путями к файлам. Проще говоря, вы говорите TypeScript: «Когда встретишь импорт, начинающийся с @utils/, на самом деле ищи файл в папке src/utils/».

Вот как это выглядит в коде:

// tsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@utils/*": ["src/utils/*"],
      "@components/*": ["src/components/*"]
    }
  }
}

Теперь вместо ужасных относительных путей вы пишете:

// Было (кошмар)
import { formatDate } from '../../../../utils/dateHelpers';

// Стало (красота)
import { formatDate } from '@utils/dateHelpers';

Согласитесь, разница колоссальная. Но как TypeScript понимает, куда идти?

Как работает резолвинг модулей под капотом

Чтобы понять path mapping в TypeScript, нужно разобраться с базовым механизмом resolution (разрешения модулей). Когда TypeScript компилирует ваш код, он использует стратегию, похожую на Node.js, но с дополнительными возможностями.

Процесс выглядит так:

  1. Проверка на абсолютный путь (начинается с /) — если да, ищет от корня файловой системы.
  2. Проверка на относительный путь (начинается с ./ или ../) — резолвит относительно текущего файла.
  3. Проверка на алиас из paths — если импорт начинается с префикса, определённого в paths, TypeScript подменяет его на соответствующий путь из маппинга.
  4. Поиск в node_modules — если ничего не подошло, ищет модуль как npm-пакет.

Ключевой момент: TypeScript сам не меняет физические пути в скомпилированном JavaScript. Он просто проверяет типы и выдаёт ошибки, если файл не найден. А вот замена алиасов на реальные пути в рантайме — это задача сборщика (webpack, Vite, tsc-alias).

Почему paths не работают «из коробки»?

Вот самый частый вопрос на Stack Overflow: «Я настроил paths, но при запуске приложения вылетает ошибка "Cannot find module"». Причина проста: TypeScript — это транспилятор, а не бандлер.

Когда вы запускаете tsc, он генерирует JS-файлы, но оставляет алиасы нетронутыми. Например, import { foo } from '@utils/bar' останется в JS как есть. Node.js (или браузер) понятия не имеет, что такое @utils, и выдаёт ошибку.

Чтобы это исправить, нужно использовать одно из решений:

  • tsc-alias — утилита, которая после компиляции заменяет алиасы на реальные пути.
  • module-alias — npm-пакет для регистрации алиасов в Node.js.
  • Настройка сборщика (webpack resolve.alias, Vite alias, Rollup @rollup/plugin-alias).

Пример для webpack:

// webpack.config.js
const path = require('path');

module.exports = {
  resolve: {
    alias: {
      '@': path.resolve(__dirname, 'src/'),
      '@utils': path.resolve(__dirname, 'src/utils/')
    }
  }
};

Только после этого алиасы заработают и в рантайме.

Подводные камни и лучшие практики

1. Не забывайте про baseUrl

baseUrl — это базовая директория, от которой отсчитываются все пути в paths. Если вы укажете "baseUrl": ".", то "@/*": ["src/*"] будет означать ./src/*. Если baseUrl не задан, paths могут вести себя неочевидно.

2. Шаблоны путей — это не регулярные выражения

Звёздочка (*) в paths работает как glob-паттерн, а не как полноценный regex. Она захватывает всё, что идёт после префикса. Например, "@utils/*": ["src/utils/*"] — всё после @utils/ будет подставлено в src/utils/.

3. Алиасы не должны конфликтовать с npm-пакетами

Не используйте алиасы, которые совпадают с именами популярных пакетов. "react/*": ["src/react/*"] — плохая идея, так как вы перекроете доступ к настоящему React.

4. Порядок имеет значение

TypeScript проверяет пути в порядке их объявления. Если у вас есть "@/*" и "@utils/*", то более специфичный алиас должен быть выше, иначе общий шаблон перехватит импорт.

// Правильно
"@utils/*": ["src/utils/*"],
"@/*": ["src/*"]

// Неправильно — @/ перехватит @utils/
"@/*": ["src/*"],
"@utils/*": ["src/utils/*"]

Продвинутые техники: динамические алиасы и монорепозитории

В крупных проектах (монорепозиториях) paths в tsconfig.json используются для организации межпакетных зависимостей. Например, вы можете настроить алиасы так, чтобы пакет @myapp/shared ссылался на локальную папку packages/shared/src.

{
  "compilerOptions                

Блоги

Рекомендаций по книгам