jsconfig.json option

module

  • Default commonjs
  • Tested with TypeScript 5.9

TypeScript describes module in terms of the code it writes, but with noEmit nothing is written, and module still matters: it tells the checker which module system your files live in. That decides whether each .js file is an ES module or CommonJS, which syntax is allowed in it (top-level await, import.meta), and, unless you set moduleResolution yourself, how imports are found.

In a JavaScript project there are two good answers. "nodenext" follows Node.js: package.json "type", .mjs and .cjs, extensions on relative imports, exports maps. "preserve" with moduleResolution: "bundler" fits code that a bundler or a runtime such as Bun reads first. Leaving module out is the one bad answer: in TypeScript 5.9 it then defaults to commonjs, as the first example shows.

At a glance

Write "nodenext" for code Node.js runs; "preserve" with moduleResolution: "bundler" for code a bundler, Bun or another tool reads first
On the command line --module
Type string
Default in TypeScript 5.9 commonjs (with the 5.9 default target, es5; es2015 for newer targets)
Default from TypeScript 6.0 esnext (TypeScript 6.0)
TypeScript's description Specify what module code is generated.
In the quick-start setups

The description is quoted from TypeScript's own option list (Apache-2.0); see the notices. The setup values are the checked configs on the quick-start pages.

Examples

main.js Error confirmed 5.9.3 TypeScript 5.9.3, run 21 October 2026: main.js(4,15): error TS1378: Top-level 'await' expressions are only allowed when the 'module' option is set to 'es2022', 'esnext', 'system', 'node16', 'node18', 'node20', 'nodenext', or 'preserve', and the 'target' option is set to 'es2017' or higher. main.js(4,53): error TS1343: The 'import.meta' meta-property is only allowed when the '--module' option is 'es2020', 'es2022', 'esnext', 'system', 'node16', 'node18', 'node20', or 'nodenext'. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. main.js(4,15): error TS1378: Top-level 'await' expressions are only allowed when the 'module' option is set to 'es2022', 'esnext', 'system', 'node16', 'node18', 'node20', 'nodenext', or 'preserve', and the 'target' option is set to 'es2017' or higher. main.js(4,53): error TS1343: The 'import.meta' meta-property is only allowed when the '--module' option is 'es2020', 'es2022', 'esnext', 'system', 'node16', 'node18', 'node20', or 'nodenext'. 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. main.js(4,15): error TS1378: Top-level 'await' expressions are only allowed when the 'module' option is set to 'es2022', 'esnext', 'system', 'node16', 'node18', 'node20', 'nodenext', or 'preserve', and the 'target' option is set to 'es2017' or higher. main.js(4,53): error TS1343: The 'import.meta' meta-property is only allowed when the '--module' option is 'es2020', 'es2022', 'esnext', 'system', 'node16', 'node18', 'node20', or 'nodenext'. checked 21 October 2026
import { readFile } from "node:fs/promises";

const notes = await readFile(new URL("./notes.txt", import.meta.url), "utf8");error TS1378: Top-level 'await' expressions are only allowed when the 'module' option is set to 'es2022', 'esnext', 'system', 'node16', 'node18', 'node20', 'nodenext', or 'preserve', and the 'target' option is set to 'es2017' or higher.error TS1343: The 'import.meta' meta-property is only allowed when the '--module' option is 'es2020', 'es2022', 'esnext', 'system', 'node16', 'node18', 'node20', or 'nodenext'.
console.log(notes.length);
module commonjs (the TypeScript 5.9 default with no target): top-level await and import.meta are errors.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "commonjs",
    "types": [
      "node"
    ]
  },
  "include": [
    "*.js"
  ]
}
main.js TypeScript 5.9.3 TypeScript 5.9.3, run 21 October 2026: No errors. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. No errors. 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. No errors. checked 21 October 2026
import { readFile } from "node:fs/promises";

const notes = await readFile(new URL("./notes.txt", import.meta.url), "utf8");
console.log(notes.length);
module nodenext with "type": "module": the same file passes.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": [
      "node"
    ]
  },
  "include": [
    "*.js"
  ]
}

nodenext: Node's own rules

With module: "nodenext" (which also sets moduleResolution to nodenext), the checker reads each file the way Node.js will load it:

  • a .js file is an ES module when the nearest package.json says "type": "module", and CommonJS otherwise; .mjs files are always ES modules and .cjs files always CommonJS
  • relative imports in ES modules need their file extension (TS2835 without it), as Node.js requires
  • package imports go through the package's exports map, with the import or require condition to match the file
  • JSON imports in ES modules need with { type: "json" }
  • since TypeScript 5.8, a CommonJS file may require() an ES module, which Node.js allows without a flag from 20.19 and 22.12

node16, node18, node20 or nodenext?

nodenext moves with Node.js: each TypeScript release can teach it newer behavior. node16, node18 and node20 are frozen at that Node.js version's rules (node20 arrived with TypeScript 5.9). Use a frozen value when you need checking to stay put across TypeScript upgrades, or when you support an older Node.js; TypeScript's handbook suggests node18 for libraries. nodenext also implies target: "esnext" when you leave target out, and node20 implies es2023.

preserve and esnext: code a bundler reads

When a bundler, Bun, or a framework tool resolves your imports, Node's rules are too strict (the tool adds extensions for you) or simply not the rules that apply. module: "preserve" keeps each import as written and implies moduleResolution: "bundler"; the older pairing is module: "esnext" with moduleResolution: "bundler". Either way the checker allows extensionless relative imports and reads exports maps with the import condition.

rewriteRelativeImportExtensions

TypeScript 5.7 added rewriteRelativeImportExtensions, which turns import "./util.ts" into import "./util.js" in the files tsc writes. It does nothing for a .js project: there is no output, and imports already end in .js. It belongs to TypeScript projects that run .ts files directly, for example with Node.js type stripping.

Gotchas

  • A config without module (and without a recent target) is checked as old CommonJS in TypeScript 5.9: top-level await and import.meta are errors even in a file Node.js runs as an ES module. TypeScript 6.0 changes the default to esnext.
  • "type" in package.json matters only under node16 to nodenext; with preserve or esnext, TypeScript does not ask whether a file is CommonJS.

Questions

What module setting should a JavaScript project use?

"nodenext" if Node.js runs the files as they are. "preserve" (with "moduleResolution": "bundler") if a bundler or Bun reads them first. The quick-start setups show both.

Sources

Last reviewed 20 October 2026.