TypeScript 6 deprecates baseUrl and no longer treats it as a general module-resolution root. Most projects can remove it, put the old prefix directly into each paths target, and leave the alias names unchanged.

This is a compiler-resolution change. It does not make TypeScript aliases work automatically in Node.js, Jest, Vitest, Vite, or a published package.

Quick answer

Remove baseUrl, copy its path prefix into each paths target, and then test the aliases with the runtime or bundler that executes the built code. TypeScript path mappings only guide TypeScript resolution; they do not rewrite Node.js imports.

The common fix

Before:

{
  "compilerOptions": {
    "baseUrl": "./src",
    "paths": {
      "@app/*": ["app/*"],
      "@lib/*": ["lib/*"]
    }
  }
}

After:

{
  "compilerOptions": {
    "paths": {
      "@app/*": ["./src/app/*"],
      "@lib/*": ["./src/lib/*"]
    }
  }
}

The important detail is the ./src/ prefix. Removing baseUrl without moving its value into the path targets changes where TypeScript searches.

Why TypeScript deprecated baseUrl

baseUrl was originally designed for AMD loaders. It later became a common ingredient in path-alias examples even though paths has not required baseUrl for a long time.

TypeScript 6 makes the resolution rule explicit: path targets should say where they point. TypeScript 7 is expected to remove the deprecated behavior, so migrating while TypeScript 6 can still explain the warning is easier than waiting for a hard failure.

First find how your project uses it

Run the compiler’s resolution trace for one representative import:

npx tsc --noEmit --traceResolution > resolution.log

Then search the log for the alias:

rg "@app|@lib" resolution.log

Also search for bare imports that may depend on baseUrl without a named alias:

// This may have relied on baseUrl as a lookup root.
import { logger } from 'shared/logger';

Named aliases such as @app/* are easier to inventory than arbitrary bare imports.

If baseUrl was the lookup root

A less common project may use baseUrl without specific aliases. TypeScript’s release notes show that a catch-all path can preserve that lookup behavior temporarily:

{
  "compilerOptions": {
    "paths": {
      "*": ["./src/*"]
    }
  }
}

Treat this as a migration bridge, not a perfect final design. A broad catch-all can make an import look like a package even when it is a local file.

TypeScript paths do not rewrite runtime imports

This config can type-check:

{
  "compilerOptions": {
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "paths": {
      "@app/*": ["./src/*"]
    }
  }
}

But if emitted JavaScript still contains this:

import { createUser } from '@app/users/create-user.js';

Node needs its own way to resolve @app. TypeScript only used the mapping while checking types.

Choose one runtime strategy:

Project Safer alias strategy
Vite, Next.js, or Astro app Configure aliases through the framework or bundler too
Node app running emitted JS Prefer relative imports or Node package imports
Monorepo Use workspace packages with exports
Published library Use package exports and test the packed artifact

For native Node package imports, names must start with #:

{
  "imports": {
    "#app/*": "./dist/*"
  }
}

A migration sequence that isolates failures

  1. Pin TypeScript 6 in the lockfile and CI.
  2. Record the current tsc --noEmit result.
  3. Move the baseUrl prefix into explicit paths targets.
  4. Remove baseUrl.
  5. Run type checking and the production build.
  6. Inspect emitted imports or bundled output.
  7. Run the exact built entry point used in production.
  8. Run tests in a clean checkout.

Do not change module, moduleResolution, every alias, and the test runner in the same pull request. One resolution change at a time gives you a useful failure signal.

Monorepo example

Inside a monorepo, an alias that crosses package boundaries often hides a missing package contract:

import { Money } from '@repo/payments/src/money';

That import reaches into another package’s source. A better boundary exports the public module:

import { Money } from '@repo/payments';

The workspace package should declare its entry points in package.json. Editors, Node, tests, and consumers then share the same package boundary.

How to verify the migration

For an application:

npm ci
npm run typecheck
npm run build
node dist/server.js

For a library, pack and install it in a tiny consumer:

npm pack
npm install ../your-package-1.0.0.tgz

Check declaration files as well as JavaScript. An alias that disappears from runtime output can still leak into .d.ts files and break downstream users.

Common mistakes

  • Removing baseUrl but forgetting to prefix path targets.
  • Assuming a green editor means Node can resolve the alias.
  • Using a catch-all * forever without documenting why.
  • Importing another workspace package through its src directory.
  • Testing source through a runner while production executes emitted JavaScript.
  • Hiding the warning with an ignore flag instead of testing the migration.

Official reference