Error [TS5109: Option 'moduleResolution' must be set to 'NodeNext']

error TS5109: Option 'moduleResolution' must be set to 'NodeNext' の直し方(TypeScript 5.2 で module と moduleResolution の組み合わせが強制された)

FIX SUMMARY verified
Applies when
node:22.23.1-alpineTypeScript 5.2+ (module: NodeNext/Node16 requires a matching moduleResolution; accepted through 5.1)tsc with module: NodeNext (or Node16) paired with moduleResolution: node, after TypeScript is upgraded to 5.2 or later (5.1 accepted the same tsconfig)

Verified: reproduced in node:22.23.1-alpine, then the TS5109: Option 'moduleResolution' must be set to 'NodeNext' signature was gone after the fix (exit 0).

これまで通っていた 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 です。moduleNode16 を指定している場合も同じ TS5109 で、引数が 'Node16' に変わります(Option 'moduleResolution' must be set to 'Node16' ...)。

原因は、module: "NodeNext"moduleResolution: "node" の組み合わせが、TypeScript 5.2 以降で不整合として拒否されるようになったことです。module を Node 用の新しい解決方式(NodeNext / Node16)にしたなら、moduleResolution も対応する方式にそろえる必要があります。直し方は、moduleResolutionNodeNext に合わせるか、指定そのものを消して 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.3TS5109 で停止(終了コード 2)

TypeScript 5.2 で、Node16NodeNext 系の module に対して moduleResolution の一致が必須になりました。そのため、次のような食い違いで表面化します。

  • npm install で TypeScript が 5.2 以降に上がったpackage.jsontypescript を緩く指定していると、クリーンインストールで新しい版が入ります。ローカルの node_modules に 5.1 系が残っている環境では通り、CI やコンテナのクリーンインストールでだけ 5.2 以降が入って落ちます
  • npm install typescript@latest や、TypeScript に依存するツールの更新で上げた:ビルドチェーンのどこかが TypeScript を引き上げると、同じ tsconfig.json が通らなくなります。

まず npx tsc --version で版を確認します。5.2 以降なら、この記事の強制に当たっています。

直し方:moduleResolution を module に合わせる

moduleNodeNext にしているなら、moduleResolutionNodeNext にそろえます。

{
  "compilerOptions": {
    "module": "NodeNext",
    "moduleResolution": "NodeNext"
  }
}

module: "Node16" を使っているなら moduleResolution: "Node16" にします。あるいは moduleResolution の行を消す方法もあります。TypeScript は module から解決方式を推論するので、NodeNext なら NodeNext が使われ、エラーは出ません。

{
  "compilerOptions": {
    "module": "NodeNext"
  }
}

NodeNext 解決は、古い node(node10)解決と探し方が違います。package.jsonexportsimports フィールドや、拡張子つき相対 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" にそろえるか、行を消します。
  • TS5110Option 'module' must be set to ...)が出た:向きが逆で、moduleResolutionNode16/NodeNext にしたのに module がそれと違う(commonjs など)状態です。この場合は module 側を moduleResolution に合わせます(TS5109moduleResolution を直す指示なのに対し、TS5110module を直す指示。実測で両コードの向きを確認しています)。
  • moduleESNextCommonJS なのに似たエラーが出るESNext などに moduleResolution: "node10" を組み合わせた場合など、別の不整合の可能性があります。modulemoduleResolution の対応(新しい module には bundlerNode16/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.jsontsc --noEmit を実行すると、TS5109: Option 'moduleResolution' must be set to 'NodeNext' を出して終了コード 2
  • 修正:moduleResolutionNodeNext にそろえると、同じ tsc --noEmit が終了コード 0・シグネチャ消滅

再現から修正までは errfix の検証ハーネスが機械的に確認しています。reproduce と fix の差は tsconfig.jsonmoduleResolution の値だけで、app.ts は同一です。版境界は、同じ tsconfig.json[email protected] は終了コード 0 で受け入れ、5.2.25.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 に記録しています。

検証(machine-verified)

この修正は node:22.23.1-alpine のバージョン固定コンテナ内で再現し、修正後に TS5109: Option 'moduleResolution' must be set to 'NodeNext' のシグネチャが消えることを機械で確認しています。

verify — run-case.mjs
$ node run-case.mjs node/tsconfig-nodenext-moduleresolution-mismatch
● reproduce TS5109: Option 'moduleResolution' must be set to 'NodeNext' present ✓
● apply fix exit 0
● re-run TS5109: Option 'moduleResolution' must be set to 'NodeNext' gone ✓
PASS verified · node:22.23.1-alpine · signature gone

確認したのは上のイメージの中だけです。別の環境で直らなかった、記述が違う、という場合は 報告してください(対象と検証イメージは件名・本文に入ります)。