What are the new module and moduleResolution settings introduced by TypeScript for Node.js ECMAScript Modules?
TypeScript has introduced two new settings for working with ECMAScript Modules (ESM) in Node.js - "Node16" and "NodeNext". These settings are set in the "compilerOptions" of the tsconfig.json file like this:
{
"compilerOptions": {
"module": "NodeNext",
}
}NOTE: A common misconception is that node16 and nodenext only emit ES modules. In reality, node16 and nodenext describe versions of Node.js that support ES modules, not just projects that use ES modules. Both ESM and CommonJS emit are supported, based on the detected module format of each file. Because node16 and nodenext are the only module options that reflect the complexities of Node.js’s dual module system, they are the only correct module options for all apps and libraries that are intended to run in Node.js v12 or later, whether they use ES modules or not.
node16 and nodenext are currently identical, with the exception that they imply different target option values. If Node.js makes significant changes to its module system in the future, node16 will be frozen while nodenext will be updated to reflect the new behavior.
“Documentation - ECMAScript Modules in Node.js” (typescriptlang.org). Retrieved July 11, 2023.
What is the "type" field in a Node.js package.json and what does it control?
The "type" field in a Node.js package.json can be set to either "module" or "commonjs". This setting controls whether .js and .d.ts files are interpreted as ES modules or CommonJS modules, and defaults to CommonJS when not set.
{
"name": "my-package",
"type": "module",
"//": "...",
"dependencies": {
}
}“Documentation - ECMAScript Modules in Node.js” (typescriptlang.org). Retrieved July 11, 2023.
What are some of the key differences between ES Modules and CommonJS in Node.js?
When a file is considered an ES module, a few different rules come into play compared to CommonJS:
import/export statements and top-level await can be usedimport "./foo.js" instead of import "./foo")// ./foo.ts
export function helper() {
// ...
}
// ./bar.ts
import { helper } from "./foo"; // only works in CJS
helper();
// ./bar.ts
import { helper } from "./foo.js"; // works in ESM & CJS
helper();Imports might resolve differently from dependencies in node_modulesrequire() and \_\_dirname cannot be used directly“Documentation - ECMAScript Modules in Node.js” (typescriptlang.org). Retrieved July 11, 2023.
What are .mjs and .cjs file extensions in Node.js, and their TypeScript counterparts?
In Node.js, .mjs files are always ES modules, and .cjs files are always CommonJS modules. TypeScript supports two new source file extensions to correspond with these: .mts for ES modules and .cts for CommonJS. When TypeScript compiles these to JavaScript, it emits them as .mjs and .cjs files respectively. TypeScript also supports two new declaration file extensions: .d.mts for ES module declarations and .d.cts for CommonJS module declarations.
“Documentation - ECMAScript Modules in Node.js” (typescriptlang.org). Retrieved July 11, 2023.
How does Node.js handle interoperation between CommonJS and ES Modules?
Node.js allows ES modules to import CommonJS modules as if they were ES modules with a default export.
// @filename: helper.cts
export function helper() {
console.log("hello world!");
}
// @filename: index.mts
import foo from "./helper.cjs";
// prints "hello world!"
foo.helper();Sometimes, named exports from CommonJS modules can be used as well.
// @filename: helper.cts
export function helper() {
console.log("hello world!");
}
// @filename: index.mts
import { helper } from "./helper.cjs";
// prints "hello world!"
helper();In TypeScript, you can use the import foo = require("foo"); syntax for interoperation.
However, importing ESM files from a CJS module can only be done using dynamic import() calls.
“Documentation - ECMAScript Modules in Node.js” (typescriptlang.org). Retrieved July 11, 2023.
What is the "exports" field in package.json used for in ECMAScript modules?
The "exports" field in package.json defines entry points to a package and is an alternative to defining "main". It allows separate entry-points for CommonJS and ESM. TypeScript looks at the "import" field for ES modules and the "require" field for CommonJS modules. If a "types" condition is present, TypeScript uses that to locate type declarations. A separate declaration file is needed for each CommonJS and ES module entry point.
// package.json
{
"name": "my-package",
"type": "module",
"exports": {
".": {
// Entry-point for `import "my-package"` in ESM
"import": "./esm/index.js",
// Entry-point for `require("my-package") in CJS
"require": "./commonjs/index.cjs",
},
},
// CJS fall-back for older versions of Node.js
"main": "./commonjs/index.cjs",
}“Documentation - ECMAScript Modules in Node.js” (typescriptlang.org). Retrieved July 11, 2023.
How does TypeScript support the "exports" field in package.json for ECMAScript modules?
If you write an import from an ES module, it looks up the "import" field, and from a CommonJS module, it looks at the "require" field. If it finds them, it will look for a co-located declaration file. If you need to point to a different location for your type declarations, you can add a "types" import condition,
Important: moduleResolution needs to be set to node16, nodenext, or bundler, and resolvePackageJsonExports must not be disabled for TypeScript to follow Node.js’s package.json "exports" spec when resolving from a package directory triggered by a bare specifier node_modules package lookup.
Example:
{
"name": "my-package",
"exports": {
".": {
"import": {
"types": "./types/esm/index.d.ts", // Where TypeScript will look.
"default": "./esm/index.js" // Where Node.js will look.
},
"require": {
"types": "./types/commonjs/index.d.cts", // Where TypeScript will look.
"default": "./commonjs/index.cjs" // Where Node.js will look.
},
}
}
}Note: The "types" condition should always come first in “exports”.
“package.json “exports”” (typescriptlang.org). Retrieved December 11, 2023.
Why is it important for each entrypoint (CommonJS and ES module) to have its own declaration file in ECMAScript modules?
Every declaration file is interpreted either as a CommonJS module or as an ES module, based on its file extension and the "type" field of the package.json. The detected module kind must match the module kind that Node will detect for the corresponding JavaScript file for type checking to be correct.
Using a single .d.ts file to type both an ES module entrypoint and a CommonJS entrypoint will cause TypeScript to think only one of those entrypoints exists, causing compiler errors for users of the package.
“node_modules package lookups” (typescriptlang.org). Retrieved December 11, 2023.