TypeScript 4.7 と Native Node.js ESM

総論

TS 4.5 と TS 4.7 の差分

  • TypeScript 4.5 の時点では、Node.js ESM サポートは Nightly Build でしか利用できない実験的な機能であったが、4.7 で晴れて安定版の位置付けに
  • 4.5 の時点では --module node12 という名前だったが、4.7 以降では --module node16 に変更されている。挙動自体は node12 と同じ。Top Level Await についての解釈を考慮したときに、Node.js v12 よりも「Node.js v16 に対応している」と考えた方が綺麗だったため。
  • Type Reference Directive 内で利用可能な module-resolution オプションが追加されている
  • --moduleDetection という Compiler Option が追加された。スクリプトかそうじゃないかの挙動の判定に関係するが、基本的に気にする必要はない。

Node.js における Native ESM 対応

TypeScript における Node.js の Native ESM サポート

拡張子

--module: node16 がやってくれること

  • package.json が "type": "module" であれば、.ts を Native ESM と解釈して、Import / Export Statement をそのまま出力する(従来における --module esnext)
  • package.json が "type": "module" でなければ、.ts を CommonJS と解釈して、Import / Export Statement は require / module.exports=... に変換する(従来における --module commonjs)

Module Specifier

import "./hoge";
import "./hoge.mjs"; // ./hoge.mts ではない!

Conditional Exports と 型定義ファイルの出し分け

{
"name": "@types/hoge",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"exports": {
".": {
"import": {
"types": "./lib_esm/index.d.mts",
"default": "./lib_esm/index.mjs"
},
"require": {
"types": "./lib_cjs/index.d.cts",
"default": "./lib_cjs/index.cjs"
}
}
}
}
import fs from "node:fs";// このファイルがCommonJSとして扱われるのであればOK, ESMとして扱われるのであればerrorにしたい
console.log(__dirname);

resolution-mode による明示的な読み分け

/// <reference types="hoge" resolution-mode="require" />/// <reference types="hoge" resolution-mode="import" />
import type { Hoge } from "hoge" assert { "resolution-mode": "require" };import type { Hoge } from "hoge" assert { "resolution-mode": "import" };

--module node16 と footgun

{
"name": "hoge",
"main": "index.js",
"types": "index.d.ts",
"exports": {
".": {
"import": "./lib_esm/index.mjs",
"require": "./lib_cjs/index.cjs"
}
}
}
/* main.ts */
import * as hoge from "hoge";
  • Node.js の世界では ./lib_esm/index.mjs は利用可能なのでランタイム上は問題ない
  • TypeScript 4.7 の世界においては ./lib_esm/index.d.mts のファイルが存在しなければ、 ./lib_esm/index.mjs の型定義が解決できずにエラーになる
{
"name": "hoge",
"main": "index.js",
"types": "index.d.ts",
"exports": {
".": {
"import": {
"types": "./index.d.ts",
"default": "./lib_esm/index.mjs"
},
"require": {
"types": "./index.d.ts",
"default": "./lib_cjs/index.cjs"
}
}
}
}

ESM — CJS Interop

/* source ./lib.ts */
const x = "value";
export default x;
/* dist ./lib.js */
const x = "value";
exports.default = x;
/* dist ./lib.d.ts */
declare const x = "value";
export default x;
/* ./index.mts */import lib from "lib";console.log(lib.default); // <- default が必要

エコシステム

@types/node

Jest

ts-node

$ ts-node --esm src/index.mts
$ ts-node-esm src/index.mts

--

--

--

Front-end web developer. TypeScript, Angular and Vim, weapon of choice.

Love podcasts or audiobooks? Learn on the go with our new app.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Yosuke Kurami

Yosuke Kurami

Front-end web developer. TypeScript, Angular and Vim, weapon of choice.

More from Medium

Avoiding Circular Dependency Issues in Nest.js

How To Update NestJS

Update NestJS Logo

Deep introduction to class decorators in TypeScript

Forcing string patterns in Typescript