How to Fix: tsconfig paths with a ‘.’ in the path target break next.config.ts transpilation

5 min read

A single dot-prefixed path target in tsconfig.json can make next.config.ts fail during transpilation even though the app itself still starts fine elsewhere. The mismatch happens because Next.js config loading and application module resolution do not always go through the exact same path alias pipeline, so aliases like "@/*": ["./src/*"] can behave differently from "@/*": ["src/*"] when next.config.ts is compiled.

If you are reproducing the issue from the linked repository, the practical fix is usually to remove the leading dot from the paths target or avoid relying on TS path aliases inside next.config.ts entirely.

Understanding the Root Cause

This bug appears when next.config.ts imports code through a TypeScript alias defined in compilerOptions.paths, and that alias target uses a relative mapping such as ./src/*. While this looks valid in standard TypeScript projects, Next.js config transpilation is a special execution path.

There are two important layers involved:

  1. TypeScript path mapping describes how imports should be resolved during compilation.
  2. Next.js runtime config loading transpiles and evaluates next.config.ts before the full application bundling pipeline runs.

In regular app code, aliases are often resolved by the build toolchain in a way that is tolerant of several path formats. But when next.config.ts is transpiled early, alias expansion can be more sensitive. A mapping like:

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

may not be normalized the same way as:

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

Technically, the problem is rooted in how the config loader interprets the alias target during transpile-time resolution. The leading . introduces a relative form that can be handled differently depending on whether the resolver expects paths relative to baseUrl, already-normalized path segments, or direct filesystem-like expansion. In short, the app code and the config file are not guaranteed to share identical alias semantics.

That is why:

  • npm run dev may work initially,
  • editing next.config.ts to use the alias can trigger failure,
  • and changing ./src/* to src/* resolves the problem.

Step-by-Step Solution

The safest fix is to make your tsconfig path targets baseUrl-relative, not dot-relative.

1. Update your tsconfig paths

Change this:

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

To this:

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

This works because once baseUrl is set to ., the alias target should generally be written relative to that base, not as a second relative path with an extra ./.

2. Keep imports in next.config.ts consistent

If your config imports helpers, use the alias only after fixing the mapping:

import { someHelper } from '@/lib/some-helper'
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  reactStrictMode: true,
}

export default nextConfig

3. Restart the dev server completely

Because Next.js config loading happens early, hot reload may not fully recover from a broken config transpilation state. Stop the process and start it again:

npm run dev

4. Prefer direct relative imports in next.config.ts if you want maximum stability

Even with the fix, many teams choose not to depend on aliases in next.config.ts at all. This avoids differences between config execution and application bundling.

import { someHelper } from './src/lib/some-helper'
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  reactStrictMode: true,
}

export default nextConfig

If your goal is reliability across upgrades, this is often the most defensive approach.

5. Validate the final working setup

A stable version usually looks like this:

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  }
}

Common Edge Cases

Even after removing the leading dot, a few related issues can still break next.config.ts transpilation.

1. Missing or inconsistent baseUrl

If baseUrl is absent, path aliases may not resolve as expected. Make sure it exists and is aligned with your alias targets.

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

2. Using aliases that point outside the expected project root

Aliases like ../shared/* or deeply nested relative targets can be more fragile during config loading, especially in monorepos. In those cases, consider direct imports or workspace-aware configuration.

3. Monorepo resolution differences

If your project uses workspaces, package boundary resolution can differ between Next.js app code and config execution. A helper imported from another package may need to be consumed as a real package import rather than a local TS alias.

4. ESM and CommonJS interop problems

Some failures that look like path alias bugs are actually caused by module format mismatches. If the imported helper uses CommonJS patterns or incompatible exports, next.config.ts can fail before alias resolution is fully understood.

5. Cached dev state after config failure

After a bad config import, the dev server can remain in a broken state until restarted. Always do a full restart after changing tsconfig.json or next.config.ts.

6. Importing app-only code into the config file

If a helper imported into next.config.ts depends on browser APIs, React server/client boundaries, or environment-specific modules, the config loader may fail even if the alias itself is valid.

FAQ

Why does "./src/*" break while "src/*" works?

Because paths entries are interpreted relative to baseUrl, and some Next.js config transpilation code paths appear to handle dot-prefixed targets differently from plain base-relative targets. Removing ./ avoids that normalization mismatch.

Is this a TypeScript bug or a Next.js bug?

For this issue, it is best understood as a Next.js config loading compatibility problem around how tsconfig paths are consumed during next.config.ts transpilation. The alias shape may be acceptable in broader TypeScript usage but still fail in this specific execution path.

Should I use path aliases inside next.config.ts at all?

You can, but the most robust option is to keep next.config.ts simple and use direct relative imports. If you do use aliases, prefer baseUrl-relative targets like src/* instead of ./src/*.

The practical takeaway is simple: when defining TypeScript path aliases for a Next.js project, write them in a baseUrl-relative form. If next.config.ts needs to import local utilities, avoid dot-prefixed alias targets and restart the dev server after making the change. That resolves the reproducible bug described in the issue while also making your configuration more resilient to future Next.js internals changes.

For the original reproduction and issue context, see the linked repository.

Leave a Reply

Your email address will not be published. Required fields are marked *