Skip to content

Fix: TypeScript Path Aliases Not Working (Cannot Find Module '@/...')

FixDevs · (Updated: )

Part of:  JavaScript & TypeScript Errors

Quick Answer

How to fix TypeScript path aliases not resolving — tsconfig paths configured but imports still fail at runtime, in Jest, or after building with webpack, Vite, or ts-node.

The Error

You configure path aliases in tsconfig.json and TypeScript stops complaining, but the code fails at runtime or during bundling:

Error: Cannot find module '@/utils/helpers'
Require stack:
- /app/src/index.ts

Or in Jest:

Cannot find module '@/components/Button' from 'src/App.test.tsx'

Or the TypeScript compiler itself:

error TS2307: Cannot find module '@/api/client' or its corresponding type declarations.

The import works in the editor (no red squiggles), but fails when you actually run or build the code.

Why This Happens

tsconfig.json path aliases (compilerOptions.paths) are a TypeScript compiler feature only — they tell the TypeScript type checker how to resolve module paths. They do not affect the JavaScript runtime or bundler module resolution.

When you run the compiled JavaScript with Node.js, or bundle with webpack/Vite/esbuild, those tools have no knowledge of your tsconfig.json paths and cannot resolve the aliases. Each tool needs its own alias configuration:

  • Node.js (with ts-node or compiled JS): needs tsconfig-paths or module alias setup.
  • webpack: needs resolve.alias in webpack.config.js.
  • Vite: needs resolve.alias in vite.config.ts.
  • Jest: needs moduleNameMapper in jest.config.js.
  • esbuild / ESBuild-based tools: needs alias in the build config.

Platform and Environment Differences

tsconfig.json paths only describe what the type checker should accept. Every other tool in your pipeline resolves modules through its own mechanism, and those mechanisms differ widely by host, runtime, and bundler version.

Bundlers: Vite, Webpack, esbuild, Rollup, Parcel. Vite reads tsconfig.json paths only when the vite-tsconfig-paths plugin is installed; otherwise you configure resolve.alias in vite.config.ts. Webpack ignores tsconfig.json entirely unless you use tsconfig-paths-webpack-plugin. esbuild used directly has no awareness of tsconfig.json paths (the team chose not to implement it); you have to either run esbuild through a wrapper like tsup or pass an explicit alias option. Rollup needs @rollup/plugin-alias or rollup-plugin-tsconfig-paths. Parcel 2 reads tsconfig.json paths natively.

Runtimes: Node, Bun, Deno. Node.js by itself has no path-alias system; it follows the CommonJS or ESM resolution algorithm and looks at package.json imports plus the node_modules lookup chain. To make Node respect tsconfig.json paths at runtime you need tsconfig-paths/register (CommonJS) or a custom ESM loader. Node 20.6 added --env-file and Node 22 added --experimental-import-map, but neither reads tsconfig.json. Bun reads tsconfig.json paths automatically for both source and built code — this is one of the few cases where the runtime understands aliases without extra setup. Deno does not read tsconfig.json at all; it uses an import map (deno.json imports) which serves the same purpose but lives in a different file.

TypeScript transpilers: ts-node, tsx, swc-node, ts-node-esm. ts-node requires -r tsconfig-paths/register (CommonJS) or --loader ts-node/esm plus a separate loader for paths (ESM is messier). tsx (the esbuild-backed runner) does honor tsconfig.json paths out of the box for both CommonJS and ESM; this is often a smoother migration target. @swc-node/register reads tsconfig.json paths since 1.6.0.

Test runners: Jest, Vitest, node:test. Jest does not read tsconfig.json; you must configure moduleNameMapper. Vitest reuses Vite’s resolver, so the same vite.config.ts resolve.alias (or vite-tsconfig-paths plugin) works in tests. The built-in node:test runner has no alias support; you need a loader hook or tsconfig-paths/register.

ESM loader hooks per Node version. Node 18.6 added stable --loader. Node 20.6 deprecated --loader in favor of --import and the register() API; Node 22 makes the new API the default. If your team upgrades Node and tsconfig-paths/register stops working at runtime, the cause is usually the loader API change, not your tsconfig.json.

Monorepo tools: Nx, Turborepo, pnpm workspaces, Yarn workspaces. In an Nx monorepo, the root tsconfig.base.json paths map @org/feature-a to libs/feature-a/src/index.ts. Each app’s tsconfig.app.json extends the base. If your bundler config in apps/web/ references the app’s local tsconfig.app.json, the inherited paths flow through correctly; if it references its own minimal config, the aliases vanish. Turborepo does not handle path aliases itself — each package’s bundler still resolves them. pnpm workspaces and Yarn workspaces use workspace:* protocol in package.json, which is a different mechanism from tsconfig.json paths; mixing the two leads to imports that resolve in the editor and fail at build time because the bundler hits the workspace symlink instead of the alias.

Fix 1: Configure the Bundler (webpack)

If you use webpack, add the alias to webpack.config.js:

// webpack.config.js
const path = require("path");

module.exports = {
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "src"),
      "@components": path.resolve(__dirname, "src/components"),
      "@utils": path.resolve(__dirname, "src/utils"),
      "@api": path.resolve(__dirname, "src/api"),
    },
    extensions: [".ts", ".tsx", ".js", ".jsx"],
  },
};

This must match your tsconfig.json paths:

// tsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@components/*": ["src/components/*"],
      "@utils/*": ["src/utils/*"],
      "@api/*": ["src/api/*"]
    }
  }
}

Pro Tip: Keep tsconfig.json paths and webpack.config.js aliases in sync manually — or use the tsconfig-paths-webpack-plugin to read paths from tsconfig.json automatically:

const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin");

module.exports = {
  resolve: {
    plugins: [new TsconfigPathsPlugin()],
  },
};

Fix 2: Configure Vite

Vite uses Rollup for bundling and needs alias configuration in vite.config.ts:

// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import path from "path";

export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "./src"),
      "@components": path.resolve(__dirname, "./src/components"),
      "@utils": path.resolve(__dirname, "./src/utils"),
    },
  },
});

For TypeScript projects, also install @types/node so path and __dirname are recognized:

npm install -D @types/node

Use vite-tsconfig-paths to sync automatically:

npm install -D vite-tsconfig-paths
// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import tsconfigPaths from "vite-tsconfig-paths";

export default defineConfig({
  plugins: [react(), tsconfigPaths()],
  // No need to manually configure resolve.alias
});

vite-tsconfig-paths reads your tsconfig.json and configures Vite aliases automatically.

Fix 3: Configure Jest

Jest has its own module resolver that does not read webpack or Vite config. Add moduleNameMapper to jest.config.js:

// jest.config.js
module.exports = {
  moduleNameMapper: {
    "^@/(.*)$": "<rootDir>/src/$1",
    "^@components/(.*)$": "<rootDir>/src/components/$1",
    "^@utils/(.*)$": "<rootDir>/src/utils/$1",
    "^@api/(.*)$": "<rootDir>/src/api/$1",
  },
  // Other Jest config...
};

The regex pattern ^@/(.*)$ matches imports starting with @/ and maps them to the src/ directory. The $1 captures the rest of the path.

Or use jest-module-name-mapper to generate from tsconfig:

npm install -D jest-module-name-mapper
// jest.config.js
const { pathsToModuleNameMapper } = require("ts-jest");
const { compilerOptions } = require("./tsconfig.json");

module.exports = {
  moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, {
    prefix: "<rootDir>/",
  }),
};

ts-jest includes a helper that converts tsconfig.json paths to Jest’s moduleNameMapper format.

Fix 4: Fix ts-node and Node.js Runtime

When running TypeScript directly with ts-node or running compiled JavaScript with Node.js, path aliases are not resolved at runtime.

For ts-node — use tsconfig-paths:

npm install -D tsconfig-paths
# Run with ts-node and tsconfig-paths
ts-node -r tsconfig-paths/register src/index.ts

# Or add to package.json scripts
{
  "scripts": {
    "dev": "ts-node -r tsconfig-paths/register src/index.ts",
    "dev:watch": "ts-node-dev -r tsconfig-paths/register src/index.ts"
  }
}

Or configure in tsconfig.json via ts-node section:

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  },
  "ts-node": {
    "require": ["tsconfig-paths/register"]
  }
}

For compiled JavaScript — use module-alias:

After compiling TypeScript to JavaScript, the aliases are still in the output. Use module-alias to resolve them at runtime:

npm install module-alias
// package.json
{
  "_moduleAliases": {
    "@": "dist/src",
    "@components": "dist/src/components",
    "@utils": "dist/src/utils"
  }
}
// At the top of your entry file (dist/src/index.js)
require("module-alias/register");

Fix 5: Fix the tsconfig.json baseUrl Requirement

paths in tsconfig.json requires baseUrl to be set. Without it, TypeScript ignores the paths configuration:

Broken — missing baseUrl:

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

Fixed — add baseUrl:

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

baseUrl: "." sets the base directory for resolving non-relative module names to the directory containing tsconfig.json. The paths values are relative to baseUrl.

Common Mistake: Setting baseUrl: "src" and paths like "@/*": ["*"]. While this works for TypeScript, the actual filesystem paths differ from what bundlers expect. Use baseUrl: "." and explicit src/ prefixes in paths for clarity.

Fix 6: Fix Aliases in tsx, Bun, and Deno

If you switched from ts-node to a different runner and aliases stopped working, the resolver changed. The settings differ per tool.

tsx (npx tsx src/index.ts): tsx reads tsconfig.json paths automatically. No flags, no register file. If aliases still fail, run npx tsx --tsconfig ./path/to/tsconfig.json src/index.ts and confirm tsx is reading the file you expect. Common gotcha: tsx walks up from the entry file looking for the nearest tsconfig.json — a stray tsconfig.json in a parent directory wins.

Bun:

# bun reads tsconfig.json paths natively for both source and built code
bun run src/index.ts
bun test

For Bun, ensure compilerOptions.paths keys end with /* and values are arrays. Bun’s resolver is strict about the format. The first matching paths entry wins; the rest are ignored, which can mask config drift in monorepos.

Deno: Deno does not read tsconfig.json. Define aliases in deno.json:

{
  "imports": {
    "@/": "./src/",
    "@components/": "./src/components/"
  }
}

Use import "@components/Button.ts". The trailing slash matters in Deno import maps.

Fix 7: Fix Aliases in Vitest

Vitest reuses Vite’s resolver. If your vite.config.ts already has resolve.alias or the vite-tsconfig-paths plugin, tests inherit the same aliases. If you keep a separate vitest.config.ts, repeat the configuration:

// vitest.config.ts
import { defineConfig } from "vitest/config";
import tsconfigPaths from "vite-tsconfig-paths";

export default defineConfig({
  plugins: [tsconfigPaths()],
  test: {
    environment: "jsdom",
  },
});

If you migrated from Jest, you do not need moduleNameMapper in Vitest. Delete the old jest.config.js once you confirm Vitest is the only test runner — leaving both behind is a frequent source of “works in Vitest, fails in Jest” reports.

Fix 8: Fix Multiple tsconfig Files

Projects often have multiple tsconfig files (e.g., tsconfig.json, tsconfig.app.json, tsconfig.node.json). Make sure the paths are in the right config:

// tsconfig.json — base config
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  }
}

// tsconfig.app.json — extends base
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "outDir": "./dist"
  },
  "include": ["src"]
}

If your bundler references tsconfig.app.json but paths are only in tsconfig.json, they are inherited via extends. Verify with:

# Check what TypeScript resolves for a specific file
npx tsc --traceResolution 2>&1 | grep "@/utils"

Still Not Working?

Restart the TypeScript language server. In VS Code, run TypeScript: Restart TS Server from the Command Palette after changing tsconfig.json. The editor caches type information and may show stale errors.

Check for rootDir conflicts. If compilerOptions.rootDir is set to src/, paths like "@/*": ["src/*"] may not resolve correctly because src/ is already the root. Try "@/*": ["./*"] instead.

Check for case sensitivity. On macOS (case-insensitive filesystem), @/Utils/helpers and @/utils/helpers both work locally but fail on Linux servers. Use consistent casing throughout.

Check the moduleResolution setting. TypeScript 5.0+ with "moduleResolution": "bundler" handles paths differently. If upgrading TypeScript, check if this setting change affects resolution.

Check the file extension in the import. With "moduleResolution": "nodenext" or "node16", you must include the .js extension in ESM imports even when the source file is .ts. import "@/utils/helpers.js" resolves; import "@/utils/helpers" does not. The alias works either way — the extension requirement applies after the alias substitutes.

Check the exports field of an aliased package. If you alias @org/feature to packages/feature/src/index.ts but the package’s own package.json declares an exports map, modern bundlers (Webpack 5, Vite, Rollup) honor the exports map and ignore your alias. Either remove the exports map or make sure your alias points to a path that the exports map exposes.

Check editor and CLI use the same tsconfig. VS Code opens the workspace root and uses the nearest tsconfig.json. The CLI build (tsc, next build, vite build) may use a sibling tsconfig.build.json. Add "references" and a project-references workflow so both paths come from one source of truth.

Check for paths entries with no array value. Every entry in paths must be a JSON array, even with a single element: "@/*": ["src/*"], not "@/*": "src/*". The non-array form is silently accepted by some tools and rejected by others, which produces “works in editor, fails in build” symptoms.

For module resolution errors in general (not alias-specific), see Fix: TypeScript Cannot Find Module. For Jest module errors, see Fix: Jest Cannot Find Module. For Node-side MODULE_NOT_FOUND errors after a tsc build, see Fix: Node ERR_MODULE_NOT_FOUND. When Webpack reports “Module not found” after you wire up aliases, see Fix: Webpack Module Not Found.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles