Skip to main content
Advertisement

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"
}
}
Advertisement