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
| Release | TypeScript 4.7 |
|---|---|
| Released | 24 May 2022, the day of its announcement on the TypeScript blog |
| Source | TypeScript 4.7 release notes |
| Examples | Every example runs through TypeScript 5.9.3 when this site is built. |
| Previous release | TypeScript 4.6 |
| Next release | TypeScript 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.
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));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.
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'.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.
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'.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.
const label = "a";
console.log(label);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.
const key = "label";
/** @type {{ label: string | number }} */
const record = { label: "sale" };
if (typeof record[key] === "string") {
console.log(record[key].toUpperCase());
}jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": [
"*.js"
]
} /**
* @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)),
});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.
node16andnodenextwere added, not switched on: ajsconfig.jsonhas to set them.- JSDoc tags are read as before.
Upgrade notes
Moving a JavaScript project to nodenext usually means:
- Adding
.jsto relative imports in ES module files. - Setting
"type": "module"(or using.mjs) for files that useimportandexport. - Checking packages whose
exportslack atypescondition; their types may stop resolving. - Separately from
nodenext, two breaking changes:readonlytuples now have a read-onlylength, and template strings that may hold a symbol are errors.
Gotchas
- Since TypeScript 5.2,
moduleandmoduleResolutionmust match when either isnode16ornodenext; setting only"module": "nodenext"is enough.
Sources
- TypeScript 4.7 release notes · TypeScript handbook
- Announcing TypeScript 4.7 · TypeScript blog
- Modules: Choosing compiler options · TypeScript handbook
Last reviewed 20 October 2026.