Fix TypeScript Path Alias "Cannot Find Module" Error
Quick Answer
- Root cause: TypeScript's
tsccompiler does not convertpathsaliases (e.g.,@utils/*) to relative paths in compiled JavaScript output, causing Node.js to fail at runtime. - First checks: Verify
baseUrlis set intsconfig.json, confirm path alias keys match import statements exactly, and determine whether you're running TypeScript directly (ts-node) or compiled JavaScript. - Minimal fix for development: Install
tsconfig-pathsand run withnode -r tsconfig-paths/register dist/index.js. - Minimal fix for production: Install
tsc-aliasand add it to your build script:"build": "tsc && tsc-alias". - Applicable environments: Any TypeScript project using
tsconfig.jsonpathswithts-node,tsccompilation, or Node.js runtime. Not needed for projects using Webpack, Vite, or other bundlers that handle aliases natively.
What Problem It Solves
TypeScript's paths configuration in tsconfig.json lets you define import aliases like:
JSON{ "compilerOptions": { "baseUrl": ".", "paths": { "@utils/*": ["src/utils/*"], "@components/*": ["src/components/*"] } } }
This allows clean imports like import { helper } from '@utils/helper'. However, the TypeScript compiler (tsc) only uses paths for type checking—it does not rewrite these aliases to relative paths in the compiled JavaScript output. When Node.js runs the compiled files, it cannot resolve @utils/helper and throws Cannot find module.
Root Cause Analysis
The disconnect happens because:
- TypeScript compiler (
tsc) treatspathsas a type-resolution feature only - Node.js runtime has no knowledge of
tsconfig.jsonpaths - Compiled JavaScript retains the original alias import statements unchanged
This affects two main scenarios:
- Development: Running TypeScript directly with
ts-node(which can resolve paths) - Production: Running compiled JavaScript with
node(which cannot)
Common Errors and Fixes
Error: Cannot find module '@utils/helper' or its corresponding type declarations
Solution: Verify tsconfig.json configuration:
JSON{ "compilerOptions": { "baseUrl": ".", "paths": { "@utils/*": ["./src/utils/*"], "@components/*": ["./src/components/*"] } } }
Ensure baseUrl is set and path patterns match. Then install tsconfig-paths and register it:
BASHnpm install --save-dev tsconfig-paths node -r tsconfig-paths/register dist/index.js
Error: tsc compiles successfully, but Node.js runtime throws 'Cannot find module'
Solution: Use tsc-alias to rewrite aliases after compilation:
BASHnpm install --save-dev tsc-alias
Add to your package.json build script:
JSON{ "scripts": { "build": "tsc && tsc-alias", "start": "node dist/index.js" } }
Error: tsconfig-paths/register does not work with ESM
Solution: For projects with "type": "module" in package.json, use the ESM loader:
BASHnode --loader tsconfig-paths/register dist/index.js
Or better, switch to tsx which natively supports both ESM and path aliases:
BASHnpm install --save-dev tsx tsx dist/index.js
Error: Path alias works in development but fails in production
Solution: This is the classic development-production mismatch. Use tsc-alias in your production build pipeline:
JSON{ "scripts": { "build": "tsc && tsc-alias", "start:dev": "ts-node src/index.ts", "start:prod": "node dist/index.js" } }
Comparison: tsconfig-paths vs tsc-alias vs tsx
| Solution | Type | Best For | ESM Support | Production Ready |
|---|---|---|---|---|
tsconfig-paths | Runtime | Development, quick iteration | Limited (needs loader flag) | No (adds startup overhead) |
tsc-alias | Build-time | Production builds | Yes | Yes (no runtime dependency) |
tsx | Runtime | Both dev and prod | Yes (native) | Yes (but slight overhead) |
Recommendation: Use tsconfig-paths with ts-node during development, and tsc-alias in your production build pipeline. For simpler projects, consider tsx as a unified runtime that handles both development and production.
Production Notes and Security Checks
- Build pipeline: Always include
tsc-aliasas a build step in CI/CD. Missing this step will cause production failures. - Dependencies: Install
tsconfig-pathsortsc-aliasasdevDependenciesonly—they are not needed at runtime if you use build-time alias replacement. - Explicit config: Use the
--projectflag to specify yourtsconfig.jsonpath explicitly to avoid accidental use of global configurations. - Path safety: Avoid absolute paths in
pathsconfiguration (e.g.,/usr/lib/*) to prevent potential path traversal issues. - Dynamic imports: Path alias resolution does not work with dynamic
import()expressions orrequire()calls using variables. Aliases must be statically analyzable strings.
FAQ
Q: Why does tsc compile successfully but Node.js still throws 'Cannot find module'?
A: TypeScript's compiler (tsc) only uses paths for type checking and does not rewrite aliases to relative paths in the compiled JavaScript output. The compiled files retain the original alias syntax (e.g., @utils/helper), which Node.js cannot resolve. You need a separate tool like tsc-alias (build-time) or tsconfig-paths (runtime) to handle the alias resolution.
Q: tsconfig-paths vs tsc-alias—which should I use?
A: Use tsconfig-paths during development with ts-node for fast iteration—it registers a hook that resolves aliases at runtime. Use tsc-alias for production builds—it rewrites aliases to relative paths in the compiled output, eliminating runtime dependencies. For a unified approach, consider tsx which handles both development and production with native alias support.
Q: My project uses ESM ("type": "module"). How do I make path aliases work?
A: For ESM projects, tsconfig-paths requires the --loader flag: node --loader tsconfig-paths/register dist/index.js. A cleaner solution is to use tsx (tsx dist/index.js), which natively supports ESM and path aliases. Alternatively, use tsc-alias in your build step, which works with both CommonJS and ESM output.