ECMAScript Modules Flashcards

1
Q

What are the new module and moduleResolution settings introduced by TypeScript for Node.js ECMAScript Modules?

A

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.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
2
Q

What is the "type" field in a Node.js package.json and what does it control?

A

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": {
    }
}
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
3
Q

What are some of the key differences between ES Modules and CommonJS in Node.js?

A

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 used
  • Relative import paths need full extensions (e.g., import "./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_modules
  • Certain global-like values like require() and \_\_dirname cannot be used directly
  • CommonJS modules get imported under certain special rules
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
4
Q

What are .mjs and .cjs file extensions in Node.js, and their TypeScript counterparts?

A

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.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
5
Q

How does Node.js handle interoperation between CommonJS and ES Modules?

A

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.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
6
Q

What is the "exports" field in package.json used for in ECMAScript modules?

A

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",
}
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
7
Q

How does TypeScript support the "exports" field in package.json for ECMAScript modules?

A

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”.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
8
Q

Why is it important for each entrypoint (CommonJS and ES module) to have its own declaration file in ECMAScript modules?

A

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.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly