Path Aliases and Tooling Sync
Keep tsconfig paths, bundler aliases, Vitest, and ESLint in sync so @/ imports resolve everywhere — not just in the IDE.
- typescript
- path-aliases
Path aliases (@/components/Button) keep imports stable as files move. The trap: TypeScript understands paths in tsconfig, but the bundler, test runner, and ESLint each need their own mapping (or a shared plugin). When they drift, you get “works in VS Code, fails in CI.”
Docs: Module Resolution, tsconfig paths.
tsconfig base
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}
import { Button } from '@/components/Button';
baseUrl + paths are for the typechecker / editor. They do not rewrite emit by default (tsc still won’t rewrite paths unless you add a tool).
Bundler must agree
Vite
// vite.config.ts
import path from 'node:path';
import { defineConfig } from 'vite';
export default defineConfig({
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
});
Webpack
resolve: {
alias: { '@': path.resolve(__dirname, 'src') },
}
Next.js
Often reads jsconfig/tsconfig paths automatically — still verify version behavior.
Tests (Vitest / Jest)
// vitest.config.ts
export default defineConfig({
resolve: {
alias: { '@': path.resolve(__dirname, 'src') },
},
// or use vite-tsconfig-paths plugin
});
// jest
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
}
Plugin approach: vite-tsconfig-paths / tsconfig-paths so one source of truth drives multiple tools.
ESLint import plugin
// eslint.config.js snippets
settings: {
'import/resolver': {
typescript: { project: './tsconfig.json' },
},
}
Without a TypeScript resolver, import/no-unresolved false-positives on @/.
Storybook / Playwright / MSW
Each tooling process that loads your source needs the alias:
- Storybook Vite builder — share Vite config
- Playwright component testing — same as Vite/Webpack
- Node scripts using
tsx— registertsconfig-paths/registerif required
Relative vs alias policy
| Import | Prefer |
|---|---|
Deep ../../../ across features |
Alias @/ |
| Sibling in same folder | Relative ./ |
| Public package boundary | Package name |
Don’t alias everything — local relatives still read well for colocated files.
Monorepos
"paths": {
"@app/*": ["apps/web/src/*"],
"@ui/*": ["packages/ui/src/*"]
}
Prefer workspace package names ("@org/ui") with real package.json exports over infinite path mapping. Paths are a convenience; packages are a boundary.
Common failure modes
- IDE ok,
vite buildfails — missing bundler alias. - Tests fail only in CI — Jest mapper missing.
tsc --noEmitok, runtime wrong — you’re typechecking paths that never rewrite for a raw Node run.- Duplicate aliases —
@vs@/inconsistency.
// pick one style and enforce
import x from '@/lib/x';
// not sometimes '@lib/x' and sometimes 'src/lib/x'
Checklist for a new alias
- Add
pathsin the relevant tsconfig. - Mirror in bundler.
- Mirror in test runner (or use tsconfig-paths plugin).
- Teach ESLint resolver.
- Grep CI scripts for other entrypoints (codegen, e2e).
- Document in README.
Interview out-loud answer
“paths only teaches TypeScript. Runtime tools need matching aliases or a shared tsconfig-paths integration. I keep one mapping, sync Vite/Vitest/ESLint, and prefer package boundaries in monorepos over deep path hacks.”
Related on this site
- tsconfig strict flags
- Declaration files and DefinitelyTyped
- TypeScript for JavaScript engineers
- Testing Library
- Core Web Vitals
Further reading
Related guides
- Basic Types and AnnotationsPrimitives, arrays, objects, and function annotations TypeScript actually checks — plus where inference is enough and where it is not.
- Branded Types for IDsNominal-style UserId vs OrderId in TypeScript — prevent ID mixups at compile time with brands, parsers, and form boundaries.
- Conditional Types IntroT extends U ? X : Y — how TypeScript picks types from conditions, distributes over unions, and powers utility types you already use.
- Declaration Files and DefinitelyTypedHow .d.ts files describe JS to TypeScript, when to use @types packages, module augmentation, and writing minimal ambient types for untyped libs.
- Discriminated Unions for UI StateModel loading, success, and error as mutually exclusive variants so TypeScript and your UI cannot show impossible states.