これまで通っていた tsconfig.json のまま tsc を実行すると、型チェックが始まる前に次のエラーで止まることがあります。
tsconfig.json(4,25): error TS5109: Option 'moduleResolution' must be set to 'NodeNext' (or left unspecified) when option 'module' is set to 'NodeNext'.
終了コードは 2 です。module に Node16 を指定している場合も同じ TS5109 で、引数が 'Node16' に変わります(Option 'moduleResolution' must be set to 'Node16' ...)。
原因は、module: "NodeNext" と moduleResolution: "node" の組み合わせが、TypeScript 5.2 以降で不整合として拒否されるようになったことです。module を Node 用の新しい解決方式(NodeNext / Node16)にしたなら、moduleResolution も対応する方式にそろえる必要があります。直し方は、moduleResolution を NodeNext に合わせるか、指定そのものを消して TypeScript に推論させることです。
{
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "NodeNext"
}
}
なぜ起きるのか:module が解決方式を決めるのに、古い方式を明示していた
module は出力するモジュール形式を、moduleResolution は import をどう探すか(モジュール解決方式)を決めます。この2つは独立ではありません。module: "NodeNext" は、Node.js の ESM/CommonJS 判定(package.json の "type" や .mjs/.cjs 拡張子、exports フィールドなど)に沿った解決を前提にした設定です。そこに moduleResolution: "node"(古い、拡張子や exports を見ない解決方式。TypeScript では node10 とも呼ばれます)を組み合わせると、出力の前提と解決の方式が食い違います。
TypeScript 5.2 は、この食い違いを設定の誤りとして TS5109 で止めるようになりました。(or left unspecified) とあるとおり、moduleResolution を書かなければ TypeScript が module から適切な方式(NodeNext なら NodeNext)を推論するので、エラーは出ません。明示的に node と書いたときだけ、前提と矛盾するので止まります。
エラーメッセージが指す tsconfig.json(4,25) は、矛盾している moduleResolution の行と桁です。型チェックの前の設定読み込みの段階で止まるので、自分の .ts の型エラーは1件も出ません。
なぜ TypeScript 5.2 以降で出るのか
境界は TypeScript の版で、Node の版でもコードでもありません。同じ tsconfig.json を、TypeScript 5.1 は受け入れて型チェックを通し、5.2 以降は TS5109 で止めます。
| TypeScript | 同じ module: NodeNext / moduleResolution: node の挙動 |
|---|---|
| 5.1.6 | 設定を受け入れる(終了コード 0) |
| 5.2.2 / 5.9.3 | TS5109 で停止(終了コード 2) |
TypeScript 5.2 で、Node16/NodeNext 系の module に対して moduleResolution の一致が必須になりました。そのため、次のような食い違いで表面化します。
npm installで TypeScript が 5.2 以降に上がった:package.jsonがtypescriptを緩く指定していると、クリーンインストールで新しい版が入ります。ローカルのnode_modulesに 5.1 系が残っている環境では通り、CI やコンテナのクリーンインストールでだけ 5.2 以降が入って落ちます。npm install typescript@latestや、TypeScript に依存するツールの更新で上げた:ビルドチェーンのどこかが TypeScript を引き上げると、同じtsconfig.jsonが通らなくなります。
まず npx tsc --version で版を確認します。5.2 以降なら、この記事の強制に当たっています。
直し方:moduleResolution を module に合わせる
module を NodeNext にしているなら、moduleResolution も NodeNext にそろえます。
{
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "NodeNext"
}
}
module: "Node16" を使っているなら moduleResolution: "Node16" にします。あるいは moduleResolution の行を消す方法もあります。TypeScript は module から解決方式を推論するので、NodeNext なら NodeNext が使われ、エラーは出ません。
{
"compilerOptions": {
"module": "NodeNext"
}
}
NodeNext 解決は、古い node(node10)解決と探し方が違います。package.json の exports/imports フィールドや、拡張子つき相対 import(ESM では ./foo.js のように書く)を見るようになるため、そろえた直後に別の import 解決エラー(TS2307 など)が出ることがあります。それは設定が正しく NodeNext になった結果なので、import の書き方をその解決方式に合わせて直します(次節)。
切り分け(うまくいかないとき)
moduleResolutionを消したら別のエラー(TS2307: Cannot find module ...)が出る:NodeNext解決になったことで、import のパスや拡張子の扱いが変わっています。ESM として出す(package.jsonに"type": "module")なら、相対 import は./foo.jsのように出力後の拡張子つきで書きます。exportsを持つパッケージは、そのexportsが公開するサブパスしか import できません。module: "Node16"を使っている:出るのはTS5109の'Node16'版(Option 'moduleResolution' must be set to 'Node16' ...)です。対処は同じで、moduleResolution: "Node16"にそろえるか、行を消します。TS5110(Option 'module' must be set to ...)が出た:向きが逆で、moduleResolutionをNode16/NodeNextにしたのにmoduleがそれと違う(commonjsなど)状態です。この場合はmodule側をmoduleResolutionに合わせます(TS5109がmoduleResolutionを直す指示なのに対し、TS5110はmoduleを直す指示。実測で両コードの向きを確認しています)。moduleはESNext/CommonJSなのに似たエラーが出る:ESNextなどにmoduleResolution: "node10"を組み合わせた場合など、別の不整合の可能性があります。moduleとmoduleResolutionの対応(新しいmoduleにはbundlerかNode16/NodeNext)を確認します。moduleResolution: "node"は TypeScript 5.0 でnode10に改称された古い(legacy)方式で(nodeはその後方互換の別名)、公式は新規利用を勧めていません。- 手元では通るのに CI でだけ落ちる:TypeScript の版が違います。CI のログで実際に入った
typescriptの版を確認します。5.2 以降なら本記事の強制です。同じく JS/TS ツールチェーンの版で表面化するものに、削除された compiler option で止まる error TS5102: Option ’…’ has been removed の直し方 があります。
検証環境
node:22.23.1-alpine、ネットワーク有り、[email protected]- 再現:
module: "NodeNext"とmoduleResolution: "node"を書いたtsconfig.jsonでtsc --noEmitを実行すると、TS5109: Option 'moduleResolution' must be set to 'NodeNext'を出して終了コード 2 - 修正:
moduleResolutionをNodeNextにそろえると、同じtsc --noEmitが終了コード 0・シグネチャ消滅
再現から修正までは errfix の検証ハーネスが機械的に確認しています。reproduce と fix の差は tsconfig.json の moduleResolution の値だけで、app.ts は同一です。版境界は、同じ tsconfig.json を [email protected] は終了コード 0 で受け入れ、5.2.2 と 5.9.3 は終了コード 2 で止めることを実測して裏づけました。あわせて 5.9.3 で、module: "Node16" と moduleResolution: "node" の組み合わせが(TS5110 ではなく)TS5109 の 'Node16' 版を出すこと、moduleResolution の行を消すと NodeNext/Node16 のいずれでも終了コード 0 になること、逆向きの誤設定(module: "commonjs" に moduleResolution: "NodeNext")が TS5110 を出すことも実測しました。各版・構成・終了コードはケースの verification/probes.txt に記録しています。