Fix TypeScript Module Resolution Errors in Node.js ESM Projects

Topic: typescript-node-esm-module-resolution-errorUpdated 7/24/2026

Quick Answer

  • Root cause: TypeScript's moduleResolution setting must match your project's module system (ESM vs CJS) and runtime environment (Node.js vs bundler). Mismatches cause "Cannot find module" errors, path alias failures, and ESM/CJS confusion.
  • First checks: Verify your tsconfig.json has "moduleResolution": "node16" or "nodenext" for modern Node.js ESM projects, and confirm package.json contains "type": "module" if using ESM.
  • Minimal fix: Set "module": "NodeNext" and "moduleResolution": "NodeNext" in tsconfig.json, ensure all import paths include file extensions (.js), and restart your TypeScript server.
  • Environment boundary: This guidance applies to TypeScript 4.7+ with Node.js v12+; for bundlers (Vite, webpack), use "moduleResolution": "bundler" instead.

What Problem It Solves

TypeScript module resolution errors occur when the compiler cannot locate imported modules or their type declarations. This is especially common when:

  • Migrating from CommonJS to ESM
  • Using path aliases (e.g., @/components)
  • Working with monorepos containing multiple packages
  • Upgrading Node.js or TypeScript versions
  • Depending on ESM-only packages

The systematic approach described here provides a layered diagnostic method (5-level check) to identify and fix resolution failures, emphasizing maintenance cycles and signal recognition rather than one-time configuration.

Root Cause Analysis

TypeScript uses the moduleResolution setting to determine how it resolves module imports. The key strategies are:

StrategyBest ForExtension RequirementsESM Support
node16 / nodenextNode.js v12+Required (.js, .mjs, .cjs)Full
bundlerVite, webpack, esbuildOptionalPartial
node10Legacy Node.jsNot requiredNone
classicPre-1.6 TypeScriptNot requiredNone

The most common failure pattern: using bundler mode during development (which doesn't enforce file extensions) but running with Node.js ESM (which requires them). This creates a gap where TypeScript compiles successfully but Node.js throws runtime errors.

Minimal Working Configuration

For a modern Node.js ESM project, your tsconfig.json should include:

JSON
{
  "compilerOptions": {
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "target": "ES2022",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

And your package.json must include:

JSON
{
  "type": "module",
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "import": "./dist/index.js",
      "types": "./dist/index.d.ts"
    }
  }
}

Common Errors and Fixes

Error: "Cannot find module 'x' or its corresponding type declarations"

Solution (layered check):

  1. Verify the package is installed in the current workspace (npm ls x or check package.json)
  2. Check if the package publishes type declarations (look for types or typings in its package.json)
  3. Import from the package root path instead of deep paths
  4. Ensure moduleResolution matches the package format (ESM packages require node16/nodenext)

Error: Path alias imports compile in one place but fail elsewhere (Jest, ts-node)

Solution:

  1. Configure aliases in TypeScript's paths within tsconfig.json
  2. Configure separate alias mappings for runtime tools:
    • Jest: moduleNameMapper in jest.config.js
    • ts-node: use tsconfig-paths module
  3. Consider using bundler mode (e.g., with Vite) for unified resolution
  4. Restart TypeScript server and development processes

Error: ESM errors after upgrading Node.js or TypeScript

Solution:

  1. Re-evaluate your module strategy: decide ESM or CJS as the foundation
  2. Update tsconfig.json with "module": "NodeNext" and "moduleResolution": "NodeNext"
  3. Check package.json type field ("module" or "commonjs")
  4. Ensure all import statements use correct extensions (.mjs/.cjs or .js)

Error: Deep imports into packages fail after package update

Solution:

  1. Check the package's package.json exports field to see if deep paths are exported
  2. Switch to the package's public entry point (import from 'some-lib' instead of 'some-lib/dist/utils')
  3. If deep imports are necessary, consider forking the package or submitting a PR to add exports
  4. Update the package to the latest version and review changelog for export changes

Production Notes and Security Checks

Critical limitations:

  1. Path aliases (e.g., @/) are TypeScript-compile-time only — runtime tools (Node, Jest) require separate configuration:

    • Node: use tsconfig-paths or ts-node --paths
    • Jest: configure moduleNameMapper
    • Bundlers: usually handle automatically
  2. bundler mode doesn't enforce file extensions — but Node.js native ESM requires them, causing runtime errors

  3. Monorepo complexity — different packages may use different moduleResolution strategies, causing cross-package import failures

  4. Version sensitivity — upgrading Node.js or TypeScript may change module resolution behavior; always re-validate configuration

Security recommendations:

  • Avoid deep imports (some-lib/dist/internal) — use public entry points only
  • Regularly audit tsconfig.json paths and baseUrl to prevent path leakage or unauthorized access
  • Use "skipLibCheck": true in production to reduce compilation time, but verify type declarations separately

FAQ

Q: Why does my TypeScript code show "Cannot find module" in VS Code but compiles and runs fine?

A: This usually happens when VS Code uses a different TypeScript version than your project, or path aliases only work at compile time. Fix: Press Ctrl+Shift+P, select "TypeScript: Select TypeScript Version", choose "Use Workspace Version", then restart the TS server (Ctrl+Shift+P → "TypeScript: Restart TS Server"). If using path aliases, ensure paths and baseUrl are correctly configured in tsconfig.json.

Q: How do I manage module resolution across multiple packages in a monorepo?

A: Use TypeScript Project References with a shared base config. Create tsconfig.base.json with common settings (e.g., "moduleResolution": "node16"), then each sub-package's tsconfig.json extends it and sets references to dependencies. Use pnpm or yarn workspaces to ensure consistent TypeScript versions across packages. Run tsc --build regularly to build all referenced projects.

Q: My project migrated from CommonJS to ESM and now has "require is not defined" errors everywhere. How do I fix this systematically?

A: Follow this migration sequence: 1) Add "type": "module" to package.json; 2) Update tsconfig.json with "module": "NodeNext" and "moduleResolution": "NodeNext"; 3) Replace all require() with import statements, ensuring import paths include file extensions (.js); 4) For dynamic imports, use import() function or keep .cjs files; 5) Check all dependencies for ESM support (look at exports field in their package.json); 6) Update test tool configurations (Jest transforms, moduleNameMapper); 7) Run tsc --noEmit to check types, then fix runtime errors incrementally.

Official References

Related Guides