Skip to content
TypeScript ts config 3 min read

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.

ToolchainResolves paths at runtime?What to do
Vite / webpack / esbuild / Next.jsYes (reads tsconfig)Nothing extra
tsxYesNothing extra
ts-nodeNo (by default)Add tsconfig-paths
Plain tsc output run by NodeNoRun 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 paths aliases identical across tsconfig.json and your bundler/test config (Vite, Jest moduleNameMapper, etc.). Drift between them causes imports that type-check but fail at runtime or in tests.

Best Practices

  • Use paths to replace deep relative imports (../../..) with stable aliases like @/.
  • Prefer self-anchored paths ("./src/*") over relying on baseUrl in new projects.
  • Remember paths is type-check-only — it never rewrites emitted import specifiers.
  • With plain tsc, add tsc-alias (or switch to tsx) 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.
Last updated June 29, 2026
Was this helpful?