moduleResolution
Chooses the algorithm that turns an import specifier into a file on disk.
"moduleResolution": "classic" | "node10" | "node" | "node16" | "nodenext" | "bundler"- Added in TypeScript 1.6
Parameters
| Parameter | Type | Description |
|---|---|---|
$node16optional | string | Node'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. |
$nodenextoptional | string | Same as node16 but tracks the newest Node.js behaviour. Requires module nodenext. |
$bundleroptional | string | Added 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. |
$node10optional | string | Formerly 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. |
$classicoptional | string | TypeScript'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_moduleswalk-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
// 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.99When 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.
// 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);
// -> functionConditional 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.
// 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