TypeScript release

TypeScript 4.7 for JavaScript users

  • Released 24 May 2022
  • Examples run with TypeScript 5.9

TypeScript 4.7 was announced on 24 May 2022. It is the release that taught TypeScript how Node.js loads ES modules: module and moduleResolution set to node16 or nodenext follow the "type" field of package.json, the .mjs and .cjs extensions, and the exports and imports maps of packages.

For JavaScript projects that run directly in Node.js this is the setting that makes tsc agree with the runtime, and it is what this site's Node.js quick start uses. The release also added moduleDetection, narrowing for obj[key] with a constant key, and left-to-right inference for untyped callbacks in object literals.

At a glance

ReleaseTypeScript 4.7
Released24 May 2022, the day of its announcement on the TypeScript blog
SourceTypeScript 4.7 release notes
ExamplesEvery example runs through TypeScript 5.9.3 when this site is built.
Previous releaseTypeScript 4.6
Next releaseTypeScript 4.8

What changed for JavaScript

ES modules in Node.js: node16 and nodenext

With "module": "nodenext", a .js file is an ES module when the nearest package.json says "type": "module" and CommonJS otherwise. In an ES module, relative imports need their file extension, as Node.js requires, and require is not defined; import.meta and top-level await are allowed. TypeScript applies the same rules to .js files as to .ts files.

main.js Error confirmed 5.9.3 TypeScript 5.9.3, run 21 October 2026: main.js(2,29): error TS2835: Relative import paths need explicit file extensions in ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './format.js'? 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. main.js(2,29): error TS2835: Relative import paths need explicit file extensions in ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './format.js'? 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. main.js(2,29): error TS2835: Relative import paths need explicit file extensions in ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './format.js'? checked 21 October 2026
import { formatPrice } from "./format";error TS2835: Relative import paths need explicit file extensions in ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './format.js'?

console.log(formatPrice(1999));
A relative import without its extension.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "module": "nodenext",
    "moduleResolution": "nodenext",
    "target": "es2024",
    "types": ["node"],
    "skipLibCheck": true
  },
  "include": ["*.js"]
}

.mjs and .cjs files

A .mjs file is always an ES module and a .cjs file always CommonJS, whatever package.json says. An ES module can import a CommonJS file with a default import, and TypeScript checks the call through the types it reads from the CommonJS file's JSDoc. If include lists files by extension, such as *.js, add *.mjs and *.cjs to check those files too; a folder pattern such as src picks them up already.

main.js Error confirmed 5.9.3 TypeScript 5.9.3, run 21 October 2026: main.js(5,31): error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. main.js(5,31): error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'. 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. main.js(5,31): error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'. checked 21 October 2026
import math from "./math.cjs";

export const sum = math.add(1, 2);
export const wrong = math.add("1", 2);error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.
An ES module importing a .cjs file, checked.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": []
  },
  "include": [
    "*.js",
    "*.cjs"
  ]
}

Package exports, imports and self-references

TypeScript reads a package's exports map, including a types condition, which the release notes say should come first. Inside your own package, the imports field (specifiers starting with #) and imports of the package by its own name resolve as Node.js resolves them, so a JavaScript project can use Node.js's own aliases instead of paths.

src/main.js Error confirmed 5.9.3 TypeScript 5.9.3, run 21 October 2026: src/main.js(4,25): error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. src/main.js(4,25): error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'. 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. src/main.js(4,25): error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'. checked 21 October 2026
import { formatPrice } from "#lib/format.js";

console.log(formatPrice("19.99"));error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.
The Node.js way: an imports field in package.json, no paths needed (the wrong argument shows the import is checked).
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": []
  },
  "include": [
    "src/**/*.js"
  ]
}

moduleDetection

A new option decides which files are modules. The default, auto, also counts a file as a module when "type": "module" applies under node16 or nodenext; force treats every file as a module, so two plain scripts no longer share one global scope. The moduleDetection page shows both.

a.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
const label = "a";
console.log(label);
moduleDetection force: every file is a module with a scope of its own.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "preserve",
    "types": [],
    "moduleResolution": "bundler",
    "moduleDetection": "force"
  },
  "include": [
    "*.js"
  ]
}

Narrowing obj[key] and inference in object literals

Narrowing now works on an element access whose key is a literal type or a unique symbol, such as a const key = "label". And untyped callbacks in an object literal passed to a generic function are inferred left to right, so a later callback sees the type an earlier one produced.

labels.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
const key = "label";

/** @type {{ label: string | number }} */
const record = { label: "sale" };

if (typeof record[key] === "string") {
  console.log(record[key].toUpperCase());
}
obj[key] narrows when key is a constant.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": []
  },
  "include": [
    "*.js"
  ]
}
pipeline.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
/**
 * @template T
 * @param {{ produce: (input: string) => T, consume: (value: T) => void }} steps
 */
function run(steps) {
  steps.consume(steps.produce("42"));
}

run({
  produce: (input) => Number(input),
  consume: (value) => console.log(value.toFixed(1)),
});
Untyped callbacks in an object literal, inferred left to right.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": []
  },
  "include": [
    "*.js"
  ]
}

What did not change

The older settings keep their behavior: with module: commonjs or es2020 and the older resolution modes, file extensions and package.json type are not consulted.

  • node16 and nodenext were added, not switched on: a jsconfig.json has to set them.
  • JSDoc tags are read as before.

Upgrade notes

Moving a JavaScript project to nodenext usually means:

  • Adding .js to relative imports in ES module files.
  • Setting "type": "module" (or using .mjs) for files that use import and export.
  • Checking packages whose exports lack a types condition; their types may stop resolving.
  • Separately from nodenext, two breaking changes: readonly tuples now have a read-only length, and template strings that may hold a symbol are errors.

Gotchas

  • Since TypeScript 5.2, module and moduleResolution must match when either is node16 or nodenext; setting only "module": "nodenext" is enough.

Sources

Last reviewed 20 October 2026.