Path Aliases
Path aliases let you import with a friendly prefix instead of deep relative paths. import { Button } from '@/components/Button' beats '../../../../components/Button' every time.
Setup
tsconfig.json
{
"compilerOptions": {
"baseUrl": "./src",
"paths": {
"@app/*": ["./*"],
"@components/*": ["./components/*"],
"@lib/*": ["./lib/*"],
"@/*": ["./*"]
}
}
}
Use it
TS
import { Button } from '@components/Button';
import { db } from '@lib/db';
import type { User } from '@app/types';
Tell the runtime too
tsconfig only affects type checking. At runtime, your bundler (or Node) needs to know the aliases:
Vite — vite.config.ts
import { defineConfig } from 'vite';
import path from 'node:path';
export default defineConfig({
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
});
Node — tsx + tsconfig-paths
# with tsx, native to most modern bundlers # with ts-node: ts-node --require tsconfig-paths/register src/index.ts
For testing — Vitest
Vitest picks up Vite's resolve aliases automatically — set them once, they work in tests too.
For Jest
jest.config.js
module.exports = {
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
},
};
For monorepo workspaces
In workspaces, you often don't need paths at all — point package "main" at the entry file, install workspace deps, and TS resolves them through node_modules links.
Tip: Pick one convention per project. Mixing
@/foo, ~/foo, and @app/foo across files is a maintenance trap. Most teams converge on a single @/ prefix.Example
Example
// tsconfig.json
// {
// "compilerOptions": {
// "baseUrl": "./src",
// "paths": {
// "@app/*": ["./*"],
// "@components/*": ["./components/*"]
// }
// }
// }
//
// import { Button } from '@components/Button';
console.log('Path aliases tidy up deep imports');
Try it Yourself »
Exercise
Setting that maps alias prefixes to filesystem paths.
compilerOptions.
= { '@/*': ['./*'] }
Five letters.
Discussion
Loading…