paths & baseUrl
paths and baseUrl let you replace brittle relative imports like ../../../utils/format with clean aliases such as @/utils/format. baseUrl sets the directory that non-relative imports are resolved against, and paths maps alias patterns to real folders. The crucial thing to understand is that these are type-checker-only features: they tell tsc how to find declarations, but they do not rewrite your imports in the emitted JavaScript. Making aliases work at runtime requires a bundler or a tool like tsc-alias. This page covers the syntax and that all-important caveat.
baseUrl
baseUrl defines the root for resolving non-relative (“bare”) import specifiers. With it set, import x from "components/Button" is resolved starting from that base directory rather than node_modules.
{
"compilerOptions": {
"baseUrl": "."
}
}
With baseUrl: ".", an import of "src/utils/math" resolves from the project root. On its own baseUrl is rarely enough — its main job today is to provide the anchor that paths patterns are resolved against.
paths
paths maps import patterns to one or more target locations, relative to baseUrl. The * wildcard captures the rest of the specifier and substitutes it into the target.
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@components/*": ["src/components/*"],
"@config": ["src/config/index.ts"]
}
}
}
// ✅ Resolves to src/components/Button via the @components/* mapping
import { Button } from "@components/Button";
import { db } from "@/db/client";
import { env } from "@config";
Each target is an array because an alias can fall back across multiple folders — the compiler tries them in order. Patterns without a * map a single exact specifier.
paths without baseUrl
Since TypeScript 4.1, paths entries can be self-anchored, so baseUrl is optional. The mappings are then resolved relative to the tsconfig.json location. This is the cleaner modern style, especially with moduleResolution: "Bundler".
{
"compilerOptions": {
"moduleResolution": "Bundler",
"paths": {
"@/*": ["./src/*"]
}
}
}
Here no baseUrl is needed; ./src/* is interpreted relative to the config file. Use leading ./ to make the intent explicit.
The runtime caveat
This is the part that trips people up: paths only affects how the type checker resolves modules. The compiler emits your import specifiers unchanged, so import "@/db/client" stays @/db/client in the output. At runtime Node has no idea what @/ means and throws Cannot find module.
// Source compiles cleanly with paths configured...
import { db } from "@/db/client";
// ...but the emitted JS still says require("@/db/client") / import "@/db/client"
// ❌ Runtime: Error: Cannot find module '@/db/client'
Aliases work transparently only when something rewrites them. The table shows who handles it.
| Toolchain | Resolves paths at runtime? | What to do |
|---|---|---|
| Vite / webpack / esbuild / Next.js | Yes (reads tsconfig) | Nothing extra |
tsx | Yes | Nothing extra |
ts-node | No (by default) | Add tsconfig-paths |
Plain tsc output run by Node | No | Run tsc-alias after build |
Making aliases work at runtime
If you compile with tsc and run the output directly in Node, add a post-build step that rewrites the aliases into real relative paths. tsc-alias reads your paths and patches the emitted files.
npm install -D tsc-alias
# package.json
# "build": "tsc && tsc-alias"
Keep your
pathsaliases identical acrosstsconfig.jsonand your bundler/test config (Vite, JestmoduleNameMapper, etc.). Drift between them causes imports that type-check but fail at runtime or in tests.
Best Practices
- Use
pathsto replace deep relative imports (../../..) with stable aliases like@/. - Prefer self-anchored
paths("./src/*") over relying onbaseUrlin new projects. - Remember
pathsis type-check-only — it never rewrites emitted import specifiers. - With plain
tsc, addtsc-alias(or switch totsx) so aliases resolve at runtime. - For bundled apps, confirm the bundler is configured to read the same aliases.
- Mirror aliases in your test runner config to avoid test-only resolution failures.
- Keep the alias set small and consistent — one
@/*root covers most projects.