8.4 Path Mapping — tsconfig paths and @/ Alias Setup
Why Path Mapping is Needed
Nested directory structures lead to complex relative paths.
// Without path mapping — long, complex relative paths
import { UserService } from '../../../services/user.service';
import { AuthGuard } from '../../guards/auth.guard';
import { formatDate } from '../../../../utils/date';
import type { ApiResponse } from '../../../types/api';
Path aliases make this clean and readable:
// Using @/ alias — short and clear
import { UserService } from '@/services/user.service';
import { AuthGuard } from '@/guards/auth.guard';
import { formatDate } from '@/utils/date';
import type { ApiResponse } from '@/types/api';
tsconfig.json Path Mapping Setup
Basic Configuration
{
"compilerOptions": {
"baseUrl": ".", // Base for all paths
"paths": {
"@/*": ["src/*"], // @/ maps to src/
"@components/*": ["src/components/*"],
"@utils/*": ["src/utils/*"],
"@types/*": ["src/types/*"]
}
}
}
Role of baseUrl
baseUrl sets the base for absolute import paths.
{
"compilerOptions": {
"baseUrl": "./src" // src directory as base
}
}
// With baseUrl: "./src"
import { UserService } from 'services/user.service'; // ./src/services/user.service
import { formatDate } from 'utils/date'; // ./src/utils/date
paths Pattern Rules
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
// Wildcard pattern
"@/*": ["src/*"],
// Multiple candidate paths (searched in order)
"@shared/*": ["packages/shared/src/*", "libs/shared/*"],
// Exact path mapping
"@config": ["src/config/index.ts"],
"@types": ["src/types/index.ts"]
}
}
}
Configuration by Project Type
React + Vite Project
// tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}
// vite.config.ts — bundler needs same config!
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
});
// Usage
import { Button } from '@/components/ui/Button';
import { useAuth } from '@/hooks/useAuth';
import type { User } from '@/types/user';
Next.js Project
Next.js automatically recognizes paths from tsconfig.json.
// tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./*"] // Based on Next.js root
}
}
}
Or with Next.js 9.4+ supported configuration:
// jsconfig.json or tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/components/*": ["components/*"],
"@/lib/*": ["lib/*"],
"@/styles/*": ["styles/*"]
}
}
}
Node.js + Express Project
// tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@controllers/*": ["src/controllers/*"],
"@services/*": ["src/services/*"],
"@models/*": ["src/models/*"],
"@middleware/*": ["src/middleware/*"],
"@config": ["src/config/index"]
}
}
}
// src/routes/user.routes.ts
import { UserController } from '@controllers/user.controller';
import { authMiddleware } from '@middleware/auth';
import { validateBody } from '@middleware/validation';
import type { CreateUserDto } from '@models/user.model';
Important: Node.js also needs path mapping at runtime.
npm install --save-dev tsconfig-paths
// package.json
{
"scripts": {
"dev": "ts-node -r tsconfig-paths/register src/index.ts",
"start": "node -r tsconfig-paths/register dist/index.js"
}
}
Or use tsc-alias to transform paths to real paths at build time:
npm install --save-dev tsc-alias
{
"scripts": {
"build": "tsc && tsc-alias"
}
}
Monorepo Project
// tsconfig.json (root)
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@myapp/shared": ["packages/shared/src/index.ts"],
"@myapp/shared/*": ["packages/shared/src/*"],
"@myapp/ui": ["packages/ui/src/index.ts"],
"@myapp/ui/*": ["packages/ui/src/*"],
"@myapp/types": ["packages/types/src/index.ts"]
}
}
}
// apps/web/src/pages/home.tsx
import { Button, Card } from '@myapp/ui';
import { formatDate } from '@myapp/shared';
import type { User, Product } from '@myapp/types';
Bundler-Specific Alias Configuration
tsconfig paths are for type checking only. Bundlers also need configuration for runtime path resolution.
webpack
// webpack.config.js
const path = require('path');
module.exports = {
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
'@components': path.resolve(__dirname, 'src/components'),
'@utils': path.resolve(__dirname, 'src/utils'),
},
extensions: ['.ts', '.tsx', '.js', '.jsx'],
},
};
Rollup
// rollup.config.js
import alias from '@rollup/plugin-alias';
import path from 'path';
export default {
plugins: [
alias({
entries: [
{ find: '@', replacement: path.resolve(__dirname, 'src') },
],
}),
],
};
esbuild
// build.ts
import * as esbuild from 'esbuild';
import { TsconfigPathsPlugin } from '@esbuild-plugins/tsconfig-paths';
esbuild.build({
entryPoints: ['src/index.ts'],
bundle: true,
outfile: 'dist/index.js',
plugins: [TsconfigPathsPlugin({ tsconfig: './tsconfig.json' })],
});
Auto-Sync Tools
Tools that synchronize tsconfig paths with bundler configuration:
vite-tsconfig-paths
npm install --save-dev vite-tsconfig-paths
// vite.config.ts
import { defineConfig } from 'vite';
import tsconfigPaths from 'vite-tsconfig-paths';
export default defineConfig({
plugins: [tsconfigPaths()], // Auto-applies tsconfig.json paths
});
craco (Create React App)
// craco.config.js
const path = require('path');
module.exports = {
webpack: {
alias: {
'@': path.resolve(__dirname, 'src/'),
},
},
};
Complete Real-World Setup Example
my-app/
├── src/
│ ├── components/
│ ├── hooks/
│ ├── pages/
│ ├── services/
│ ├── stores/
│ ├── types/
│ └── utils/
├── tsconfig.json
└── vite.config.ts
// tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"strict": true,
"noEmit": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@components/*": ["src/components/*"],
"@hooks/*": ["src/hooks/*"],
"@pages/*": ["src/pages/*"],
"@services/*": ["src/services/*"],
"@stores/*": ["src/stores/*"],
"@types/*": ["src/types/*"],
"@utils/*": ["src/utils/*"]
}
},
"include": ["src"]
}
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import tsconfigPaths from 'vite-tsconfig-paths';
export default defineConfig({
plugins: [react(), tsconfigPaths()],
});
Pro Tips
1. Path alias design principles
Simpler is better: A single @/* is easiest to maintain
When granularity is needed: Domain-based separation (@features/*, @shared/*)
Avoid: Too many aliases (causes confusion)
2. Enabling VSCode autocomplete
If autocomplete isn't working after setting up tsconfig.json paths:
- Restart TypeScript server:
Ctrl+Shift+P→ "TypeScript: Restart TS Server" - Check TypeScript SDK path in
.vscode/settings.json
3. Jest also needs alias configuration
// jest.config.js
{
"moduleNameMapper": {
"^@/(.*)$": "<rootDir>/src/$1",
"^@components/(.*)$": "<rootDir>/src/components/$1"
}
}