Path Aliases

Difficulty: Advanced

Path aliases transform long, fragile relative imports like `import { Button } from '../../../components/ui/Button'` into clean absolute-style imports like `import { Button } from '@/components/ui/Button'`. This dramatically improves code readability and makes refactoring easier since moving a file does not break its imports if you use aliases consistently. TypeScript supports path aliases natively through the `paths` and `baseUrl` options in tsconfig.json.

The `paths` option is a map of import patterns to file locations. Patterns can use wildcards: `@/*` matches anything starting with `@/`, and `*` captures the rest of the path. The value is an array of possible locations, checked in order. The `baseUrl` option sets the base directory for non-relative module resolution. In TypeScript 4.1+, `paths` can be used without `baseUrl`, but setting `baseUrl: "."` is still common for clarity.

Critically, TypeScript path aliases only affect the type checker - they do not transform import paths in the compiled output. This means your bundler or runtime must also understand these aliases. For Vite projects, you configure `resolve.alias` in vite.config.ts. For Webpack, you use `resolve.alias` in webpack.config.js. For Jest, you use `moduleNameMapper`. For Node.js without a bundler, you need packages like `tsconfig-paths` or the newer `--experimental-specifier-resolution` flag.

Common alias patterns include `@/*` for the src directory root, `@components/*` for the components directory, `@utils/*` for utilities, and `@types/*` for shared type definitions. Some teams use shorter aliases like `~/*` instead of `@/*`. The key is consistency - pick a pattern and use it throughout the project. Avoid creating too many aliases, as they add cognitive overhead and require maintaining parallel configurations.

Code examples

Configuring Path Aliases

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

// Before aliases:
// import { Button } from '../../../components/ui/Button';
// import { formatDate } from '../../utils/date';
// import { useAuth } from '../../../hooks/useAuth';

// After aliases:
// import { Button } from '@components/ui/Button';
// import { formatDate } from '@utils/date';
// import { useAuth } from '@hooks/useAuth';

// Demonstrating the transformation
const imports = [
  { before: '../../../components/ui/Button', after: '@components/ui/Button' },
  { before: '../../utils/date', after: '@utils/date' },
  { before: '../../../hooks/useAuth', after: '@hooks/useAuth' }
];

imports.forEach(({ before, after }) => {
  console.log(`${before}`);
  console.log(`  -> ${after}`);
});

Path aliases replace deeply nested relative imports with clean, absolute-style paths. This makes the codebase more readable and resilient to file moves.

Vite + TypeScript Alias Setup

// vite.config.ts - must mirror tsconfig paths
// import { defineConfig } from 'vite';
// import path from 'path';
//
// export default defineConfig({
//   resolve: {
//     alias: {
//       '@': path.resolve(__dirname, 'src'),
//       '@components': path.resolve(__dirname, 'src/components'),
//       '@utils': path.resolve(__dirname, 'src/utils'),
//     }
//   }
// });

// Simulating alias resolution for both TS and Vite
interface AliasConfig {
  pattern: string;
  tsPath: string;
  vitePath: string;
}

const aliases: AliasConfig[] = [
  { pattern: '@/*', tsPath: 'src/*', vitePath: 'path.resolve(__dirname, "src")' },
  { pattern: '@components/*', tsPath: 'src/components/*', vitePath: 'path.resolve(__dirname, "src/components")' },
  { pattern: '@utils/*', tsPath: 'src/utils/*', vitePath: 'path.resolve(__dirname, "src/utils")' }
];

console.log('TypeScript paths:');
aliases.forEach(a => console.log(`  "${a.pattern}": ["${a.tsPath}"]`));
console.log('\nVite resolve.alias:');
aliases.forEach(a => console.log(`  "${a.pattern.replace('/*', '')}": ${a.vitePath}`));

Path aliases require dual configuration - tsconfig.json for TypeScript type checking, and vite.config.ts (or webpack.config.js) for runtime resolution.

Key points

Concepts covered

paths, baseUrl, module aliases, Vite resolve.alias, tsconfig paths, import mapping