Skip to content
TypeScript ts config 3 min read

moduleResolution

moduleResolution controls the algorithm the compiler uses to turn an import specifier like "./utils" or "lodash" into an actual file on disk. It does not change the emitted JavaScript — it changes how the type checker locates declarations and how it validates your imports. Picking the wrong value is the most common reason Cannot find module errors appear even when the file clearly exists. This page covers the modern values (NodeNext, Node16, Bundler), the legacy Node/Classic options, and how moduleResolution must stay in sync with module.

Why moduleResolution exists

Different runtimes look for modules differently. Node’s CommonJS loader tries ./x.js, then ./x/index.js; native ESM in Node requires file extensions; bundlers invent their own relaxed rules. moduleResolution tells tsc which of these strategies to emulate so the type checker matches your real runtime.

{
  "compilerOptions": {
    "module": "NodeNext",
    "moduleResolution": "NodeNext"
  }
}

Because resolution emulates a runtime, it must agree with how that runtime actually loads files. A bundler-resolved app and a Node-run script need different settings even if the source looks identical.

The values at a glance

The modern values cover almost every project. Reach for NodeNext for code Node runs directly and Bundler for anything fed through Vite, esbuild, or webpack.

ValueUse whenExtensions in imports
NodeNextModern Node (ESM + CJS via package.json)Required for relative ESM (./x.js)
Node16Same as NodeNext, pinned to Node 16 semanticsRequired for relative ESM
BundlerVite / esbuild / webpack / Next.jsOptional (extensionless allowed)
Node10 (Node)Legacy CommonJS-only projectsOptional
ClassicDeprecated — avoidn/a

Node10 is the old value formerly spelled Node; it understands package.json "main" but not "exports", so it misresolves many modern packages.

NodeNext and Node16

NodeNext implements Node’s real dual-format resolution: it honors the "exports" and "imports" maps in package.json, respects the "type" field, and understands .mts/.cts extensions. It also enforces that relative ESM imports include a file extension.

{
  "compilerOptions": {
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "target": "ES2022"
  }
}
// ✅ Correct under NodeNext — extension required for relative ESM imports
import { parse } from "./config.js";

// ❌ Error: Relative import paths need explicit file extensions
import { parse } from "./config";

Note that you write .js even though the source file is config.ts — the extension refers to the emitted file, which is what Node will actually load.

Bundler resolution

moduleResolution: "Bundler" (added in TS 5.0) models how bundlers resolve: it reads "exports" like NodeNext but lets you omit file extensions and does not force ESM extension rules. It is the right choice for front-end apps and most full-stack frameworks.

{
  "compilerOptions": {
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "target": "ESNext"
  }
}
// ✅ Extensionless relative imports are fine under Bundler
import { Button } from "./components/Button";
import { clamp } from "@/utils/math";

Use Bundler only when a bundler is actually in the pipeline — Node itself will not resolve extensionless ESM imports at runtime, so this setting would lie to the type checker if you ran the output directly.

Keeping module and moduleResolution aligned

module and moduleResolution are validated as a pair. NodeNext module requires NodeNext resolution; Bundler resolution requires a non-CommonJS module. Mismatches surface as compiler errors or, worse, type-check success that fails at runtime.

// ❌ Error: Option 'module' must be 'nodenext' when 'moduleResolution' is 'nodenext'
{
  "compilerOptions": {
    "module": "CommonJS",
    "moduleResolution": "NodeNext"
  }
}

The safe combinations are module: "NodeNext" with moduleResolution: "NodeNext", and module: "ESNext" with moduleResolution: "Bundler". Decide based on how the code runs, then set both to the matching pair.

Best Practices

  • Use moduleResolution: "NodeNext" for code Node executes directly, with module: "NodeNext".
  • Use moduleResolution: "Bundler" for Vite/esbuild/webpack apps, with module: "ESNext".
  • Avoid the legacy Node10/Node value — it ignores "exports" and misresolves modern packages.
  • Never use Classic; it predates Node resolution and breaks almost everything.
  • Under NodeNext, write .js extensions in relative imports (they point at emitted files).
  • Always keep module and moduleResolution as a matching pair to avoid runtime surprises.
  • If imports fail to resolve, check this option before suspecting a missing dependency.
Last updated June 29, 2026
Was this helpful?