← Курс/Path Aliases: абсолютные пути в проекте#188 из 257+20 XP

Path Aliases: абсолютные пути в проекте

Проблема относительных путей

В больших проектах импорты с относительными путями быстро становятся нечитаемыми:

// Без алиасов — боль
import { UserService } from '../../../services/UserService'
import { formatDate } from '../../../../utils/date'
import { Button } from '../../components/ui/Button'

При переносе файла все пути нужно исправлять вручную.

Настройка в tsconfig.json

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@components/*": ["src/components/*"],
      "@utils/*": ["src/utils/*"],
      "@hooks/*": ["src/hooks/*"],
      "@types/*": ["src/types/*"],
      "@api/*": ["src/api/*"]
    }
  }
}

Теперь импорты становятся чистыми:

// С алиасами
import { UserService } from '@/services/UserService'
import { formatDate } from '@utils/date'
import { Button } from '@components/ui/Button'

baseUrl

baseUrl задаёт корневую директорию для разрешения не-относительных импортов. При "baseUrl": "." (корень проекта) или "baseUrl": "src" можно импортировать без @:

{ "baseUrl": "src" }
// При baseUrl: "src" — работает без @
import { Button } from 'components/Button'
import { api } from 'api/client'

Настройка бандлера

TypeScript понимает алиасы, но **бандлер также нужно настроить** — иначе runtime не знает, что @/ означает src/.

Vite (vite.config.ts):

import { defineConfig } from 'vite'
import path from 'path'

export default defineConfig({
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
})

webpack (webpack.config.js):

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

**Next.js** — поддерживает @/* из коробки (начиная с Next.js 13).

Автоматическая синхронизация tsconfig ↔ Vite

Плагин vite-tsconfig-paths читает paths из tsconfig автоматически:

npm install -D vite-tsconfig-paths
// vite.config.ts
import tsconfigPaths from 'vite-tsconfig-paths'

export default defineConfig({
  plugins: [tsconfigPaths()],
})

Теперь синхронизировать tsconfig и vite.config не нужно.

Зачем использовать алиасы

1. **Читаемость** — @/services/auth vs ../../../services/auth

2. **Рефакторинг** — переместить файл, не меняя импорты

3. **Единый стиль** — все пути начинаются с @/ по всему проекту

4. **IDE поддержка** — автодополнение работает по алиасам

Типичные соглашения

| Алиас | Папка |

|-------|-------|

| @/* | src/* |

| @components/* | src/components/* |

| @utils/* | src/utils/* |

| ~/* | src/* (альтернатива @) |

Примеры

Симуляция резолвера path aliases: как бандлер преобразует @-импорты в реальные пути

// Path aliases — это просто замена одной части пути на другую.
// Симулируем, как Vite/webpack резолвит алиасы во время сборки.

// --- Конфигурация алиасов (как в tsconfig paths + vite alias) ---
const pathAliases = {
  '@': '/project/src',
  '@components': '/project/src/components',
  '@utils': '/project/src/utils',
  '@hooks': '/project/src/hooks',
  '@api': '/project/src/api',
  '@types': '/project/src/types',
}

// --- Резолвер алиасов ---
function resolveAlias(importPath, aliases) {
  // Сортируем алиасы по длине (длинные имеют приоритет)
  const sortedAliases = Object.entries(aliases)
    .sort(([a], [b]) => b.length - a.length)

  for (const [alias, realPath] of sortedAliases) {
    // Алиас с /* в конце
    if (importPath.startsWith(alias + '/')) {
      return realPath + importPath.slice(alias.length)
    }
    // Точное совпадение
    if (importPath === alias) {
      return realPath
    }
  }

  // Относительный путь — не трогаем
  if (importPath.startsWith('.')) {
    return importPath
  }

  return null // node_modules или неизвестный модуль
}

// --- Анализ зависимостей файла ---
function analyzeImports(fileContent, currentFile, aliases) {
  const importRegex = /imports+.*?froms+['"]([^'"]+)['"]/g
  const imports = []
  let match

  while ((match = importRegex.exec(fileContent)) !== null) {
    const rawPath = match[1]
    const resolved = resolveAlias(rawPath, aliases)
    imports.push({
      raw: rawPath,
      resolved: resolved || rawPath,
      isAlias: resolved !== null && !rawPath.startsWith('.'),
      isRelative: rawPath.startsWith('.'),
    })
  }

  return imports
}

// --- Симуляция файла с алиасами ---
const exampleFile = `
import { useState } from 'react'
import { Button } from '@components/ui/Button'
import { formatDate } from '@utils/date'
import { useAuth } from '@hooks/useAuth'
import { fetchUsers } from '@api/users'
import { UserType } from '@/types/user'
import { helpers } from './helpers'
import lodash from 'lodash'
`

console.log('=== Резолвинг path aliases ===')
const imports = analyzeImports(exampleFile, '/project/src/pages/Users.tsx', pathAliases)

imports.forEach(({ raw, resolved, isAlias, isRelative }) => {
  const type = isAlias ? '[ALIAS]' : isRelative ? '[RELATIVE]' : '[EXTERNAL]'
  console.log(`${type} "${raw}"`)
  if (isAlias) {
    console.log(`  -> "${resolved}"`)
  }
})

// --- Сравнение: с алиасами vs без ---
console.log('\n=== Сравнение путей ===')
const deepFilePath = '/project/src/features/dashboard/widgets/Chart.tsx'

function getRelativePath(from, to) {
  // Упрощённая симуляция
  const depth = from.split('/').length - 4
  return '../'.repeat(depth) + to.replace('/project/src/', '')
}

const imports_to_resolve = [
  '/project/src/components/ui/Button',
  '/project/src/utils/formatDate',
  '/project/src/hooks/useAuth',
]

console.log('Файл: src/features/dashboard/widgets/Chart.tsx')
console.log('\nБез алиасов:')
imports_to_resolve.forEach(imp => {
  console.log('  import from "' + getRelativePath(deepFilePath, imp) + '"')
})

console.log('\nС алиасами (@/ = src/):')
imports_to_resolve.forEach(imp => {
  const aliasPath = '@/' + imp.replace('/project/src/', '')
  console.log(`  import from "${aliasPath}"`)
})