ECMAScript Modules Flashcards
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-levelawait
can be used - Relative import paths need full extensions (e.g.,
import "./foo.js"
instead ofimport "./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 innode_modules
- Certain global-like values like
require()
and\_\_dirname
cannot be used directly - CommonJS modules get imported under certain special rules
“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.