How to Migrate From baseUrl in tsconfig for TypeScript 7

typescript baseurl tsconfig path aliases migration

TypeScript 7 deprecates the baseUrl option in tsconfig.json when you use it solely for path resolution. The replacement is the paths option, which has supported standalone usage (without baseUrl) since TypeScript 4.1. If you only used baseUrl to enable non-relative imports or path aliases, remove it entirely and define explicit entries in paths instead.

Before: the Old baseUrl Pattern

Most projects set baseUrl to "." or "./src" so that imports like import { db } from "lib/database" resolve without relative paths. Here's what that typically looked like.

// tsconfig.json (DEPRECATED pattern — remove baseUrl).
{
  "compilerOptions": {
    "baseUrl": "./src",
    "paths": {
      "@components/*": ["components/*"],
      "@utils/*": ["utils/*"]
    }
  }
}

After: Explicit Paths Without baseUrl

Drop baseUrl and make every paths entry a path relative to the tsconfig.json location. Since TypeScript 4.1, paths resolves relative to tsconfig.json when baseUrl is absent. The critical change is that every value in paths must now include the full relative prefix.

// tsconfig.json (TypeScript 7-compatible).
{
  "compilerOptions": {
    "paths": {
      "@components/*": ["./src/components/*"],
      "@utils/*": ["./src/utils/*"]
    }
  }
}

Handling Bare Module Imports That Relied on baseUrl

Some codebases used baseUrl not for aliases but to write bare specifiers like import { handler } from "api/routes" that mapped to src/api/routes.ts. These imports break when you remove baseUrl because TypeScript treats them as package names. You have two options: add them to paths explicitly, or refactor to relative imports. Adding them to paths is the non-breaking approach.

// Map bare specifiers that previously relied on baseUrl.
{
  "compilerOptions": {
    "paths": {
      "api/*": ["./src/api/*"],
      "lib/*": ["./src/lib/*"],
      "models/*": ["./src/models/*"]
    }
  }
}

Update Your Bundler or Runtime Resolver

TypeScript's paths only affects type checking. It doesn't rewrite import specifiers in the emitted JavaScript. You still need a runtime resolver. If you use Vite, webpack, Jest, or Node.js subpath imports, those configurations must match. Here are the three most common setups.

// vite.config.ts — resolve aliases to match tsconfig paths.
import { defineConfig } from "vite";
import path from "node:path";

export default defineConfig({
  resolve: {
    alias: {
      "@components": path.resolve(__dirname, "src/components"),
      "@utils": path.resolve(__dirname, "src/utils"),
    },
  },
});
// jest.config.ts — moduleNameMapper mirrors tsconfig paths.
export default {
  preset: "ts-jest",
  moduleNameMapper: {
    "^@components/(.*)$": "/src/components/$1",
    "^@utils/(.*)$": "/src/utils/$1",
  },
};

The Node.js-Native Alternative: package.json Imports

If you target Node.js 16+ and want zero build-tool configuration, use the imports field in package.json. Entries must start with #. This approach works at runtime without any bundler plugin, and TypeScript 5.4+ resolves these natively when moduleResolution is set to "bundler" or "nodenext". This is the cleanest long-term solution for Node.js projects.

// package.json — subpath imports (works at runtime in Node 16+).
{
  "imports": {
    "#components/*": "./src/components/*",
    "#utils/*": "./src/utils/*"
  }
}

Gotchas and Edge Cases for baseUrl Migration

Gotcha 1: paths entries are now relative to tsconfig.json, not baseUrl. This is the number-one migration mistake. If you had baseUrl: "./src" and paths: { "@lib/*": ["lib/*"] }, removing baseUrl without updating paths means TypeScript now looks in ./lib/* (project root) instead of ./src/lib/*. Every paths entry needs the ./src/ prefix added.

Gotcha 2: composite projects and project references. If your monorepo uses baseUrl in a root tsconfig.json that child projects inherit, each child's paths resolve relative to that child's own tsconfig.json. After you remove baseUrl from the root, verify that extended configs still resolve correctly. You might need to override paths in each child.

Gotcha 3: tsc-alias and tsconfig-paths still work. Tools like tsconfig-paths (for Node.js runtime) and tsc-alias (for post-compile rewriting) read paths directly and don't require baseUrl. You don't need changes on the tooling side. Make sure that your paths values are correct after removing baseUrl.

Gotcha 4: baseUrl is only deprecated for path resolution. If you genuinely need to change the root directory for output file structure (rare), that's the job of rootDir, not baseUrl. Audit whether your build relied on baseUrl affecting output directory layout and switch to an explicit rootDir if so.

← Back to all articles