moduleResolution

Chooses the algorithm that turns an import specifier into a file on disk.

Signature
"moduleResolution": "classic" | "node10" | "node" | "node16" | "nodenext" | "bundler"
  • Added in TypeScript 1.6

Parameters

ParameterTypeDescription
$node16optionalstringNode's dual algorithm, pinned. Uses the import or the require resolution rules depending on how the importing file will be emitted. Supports package.json "exports", "imports", self-name imports and typesVersions. Requires a node* module value.
$nodenextoptionalstringSame as node16 but tracks the newest Node.js behaviour. Requires module nodenext.
$bundleroptionalstringAdded in TypeScript 5.0. Models what webpack, Vite, esbuild, Parcel and Bun do: reads "exports" and "imports", but never requires a file extension on a relative path and allows directory/index resolution. Requires module esnext or preserve.
$node10optionalstringFormerly spelled "node". The pre-Node-12 CommonJS algorithm: node_modules walk-up, index files, extensionless paths. Ignores "exports" and "imports". Deprecated in TypeScript 6.0.
$classicoptionalstringTypeScript's original pre-1.6 algorithm. Removed in TypeScript 6.0.

Return value

Selects the lookup algorithm used to map an import specifier to a declaration or source file.

Errors

Errors when the value contradicts module: "bundler" requires module "esnext" or "preserve", and "node16"/"nodenext" require a matching node* module value. Under node16/nodenext, a relative import in an ESM file without a file extension is error TS2835.

Changelog

  • PHP 4.7Added node16 and nodenext, the first modes that read package.json "exports" and "imports" and distinguish import from require resolution.
  • PHP 5.0Added bundler, and renamed "node" to "node10" (the old spelling still works).
  • PHP 6.0classic was removed and node10 was deprecated; migrate to nodenext for Node.js or bundler for bundlers and Bun.

moduleResolution is the algorithm TypeScript uses to answer one question: given import x from "y", which file holds the types for y? Get it wrong and you see Cannot find module 'y' or its corresponding type declarations even though the package is installed and your bundler resolves it fine.

The modes that matter today

  • nodenext / node16 — mirrors Node.js. It reads the "exports" map, honours "imports" and self-name imports, and crucially picks the import or the require branch of a conditional export depending on how the importing file will be emitted.
  • bundler — reads the same "exports" and "imports" maps but keeps the conveniences bundlers give you: extensionless relative paths and directory/index resolution.
  • node10 — the old node_modules walk-up. It cannot see "exports" at all, which is why modern packages resolve to nothing or to the wrong stale types under it.

What trips people up

The value is constrained by module, and its default is derived from it, so people change module and are surprised when resolution changes too. The other shock is extensions: under node16/nodenext, a relative import inside an ESM file must write ./money.js, the emitted name, even though the file on disk is money.ts.

Example

Example · typescript
// tsconfig.json
//   { "compilerOptions": { "module": "nodenext", "moduleResolution": "nodenext" } }

// In an ESM file, a relative import must spell the EMITTED extension:
//   import { formatCents } from "./money.js";  // resolves ./money.ts  ✔
//   import { formatCents } from "./money";     // error TS2835        ✘
// Under "moduleResolution": "bundler", both forms resolve.

export function formatCents(cents: number): string {
  return `$${(cents / 100).toFixed(2)}`;
}

console.log(formatCents(1999));
// -> $19.99

When to use it

  • Fixing "Cannot find module" for a package that only ships a package.json "exports" map.
  • Matching type checking to a Vite or webpack build that allows extensionless imports.
  • Making conditional exports resolve to the ESM types instead of the CommonJS ones.
  • Migrating a legacy project off node10 before its deprecation bites.

Frequently asked questions

Should I use moduleResolution bundler or nodenext?

Use bundler when a bundler or Bun resolves your imports, and nodenext when Node.js loads the output directly. bundler allows extensionless relative imports and directory indexes; nodenext enforces Node's stricter rules, including explicit .js extensions in ES modules.

Why does TypeScript say it cannot find a module that is clearly installed?

Usually because moduleResolution is node10, which ignores the package.json "exports" field that modern packages use to point at their types. Switching to nodenext or bundler makes the exports map visible and resolves the package.

Do I have to write .js in imports when the file is .ts?

Yes, under moduleResolution node16 or nodenext inside an ES module, because the specifier must match what will exist at runtime. TypeScript maps ./money.js back to money.ts during checking; under bundler the extension can be omitted.

More examples

Why node10 cannot see a modern package

node10 predates the "exports" field entirely, so a package that ships only an exports map is invisible to it. This is the single most common cause of "Cannot find module" on an installed dependency, and the fix is switching the mode rather than adding a @types package.

Example · typescript
// node_modules/chalk/package.json
// {
//   "type": "module",
//   "exports": {
//     ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" }
//   }
// }

import chalk from "chalk";

// moduleResolution: "node10"   -> ignores "exports", looks for "main"/"types",
//                                 finds neither: error TS2307.
// moduleResolution: "nodenext" -> reads "exports", picks ./dist/index.d.ts. ✔
// moduleResolution: "bundler"  -> same as nodenext. ✔

console.log(typeof chalk);
// -> function

Conditional exports resolve differently per importing file

Only node16/nodenext model the import-versus-require split, so the two files can legitimately receive different types from the same package. bundler reads the same map but chooses the branch from the syntax used rather than from the emitted format.

Example · typescript
// node_modules/pkg/package.json
// { "exports": { ".": {
//     "import":  { "types": "./esm/index.d.mts", "default": "./esm/index.mjs" },
//     "require": { "types": "./cjs/index.d.cts", "default": "./cjs/index.cjs" }
// } } }

// app.mts  — will be emitted as ESM
import { version } from "pkg";      // types come from ./esm/index.d.mts

// app.cts  — will be emitted as CommonJS
import pkg = require("pkg");        // types come from ./cjs/index.d.cts

console.log(version, typeof pkg);
// -> 3.1.0 object

Discussion

  • Be the first to comment on this lesson.