Next.js の moduleResolution を TypeScript 7.0 目線で見直す

はじめに

普段開発している Next.js プロジェクトを VS Code で確認していたところ、tsconfig.jsonmoduleResolution: "node" に関するエラーが表示されていました。

moduleResolution は、TypeScript が import の参照先をどのように解決するかを決める設定です。普段の開発ではあまり意識しない項目ですが、Node.js、Next.js、バンドラー、TypeScript のバージョンによって適切な値が変わります。

今回エラーになっていた moduleResolution: "node" は、現在の TypeScript では node10 相当の古い解決方式として扱われます。

TypeScript 7.0 では、TypeScript 6.0 で非推奨となった一部の設定がハードエラーとして扱われるため、事前に見直しておく必要があります。その中に moduleResolution: "node" の解決方式も含まれています。

この記事では、VS Code 上で表示された moduleResolution: "node" のエラーをきっかけに、Next.js アプリケーションの tsconfig.json をどのように見直すかを整理します。

今回確認した tsconfig.json

今回確認した tsconfig.json は以下です。

{
  "compilerOptions": {
    "allowJs": true,
    "allowSyntheticDefaultImports": true,
    "module": "esnext",
    "moduleResolution": "node",
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "preserveConstEnums": true,
    "removeComments": false,
    "sourceMap": true,
    "strict": false,
    "strictPropertyInitialization": false,
    "strictNullChecks": false,
    "target": "esnext",
    "forceConsistentCasingInFileNames": true,
    "esModuleInterop": true,
    "isolatedModules": true,
    "resolveJsonModule": true,
    "noFallthroughCasesInSwitch": true
  }
}

この中で今回の主な確認対象は、以下の設定です。

"moduleResolution": "node"

moduleResolution とは何か

moduleResolution は、TypeScript が import の参照先をどのように探すかを決める設定です。

たとえば、次のような import があったとします。

import { Button } from "./Button";
import dayjs from "dayjs";

このとき TypeScript は、./Buttondayjs がどのファイル、またはどの型定義を指しているのかを解決する必要があります。

具体的には、次のようなものを探します。

  • .ts / .tsx / .d.ts などのファイル
  • node_modules 配下のパッケージ
  • package.json の types / exports / imports
  • index.ts のような省略されたファイル
  • paths で定義されたエイリアス

この探し方のルールを決めるのが moduleResolution です。

注意したいのは、moduleResolution は TypeScript の型チェック時の解決ルールであり、実際にブラウザや Node.js がコードを実行するときの解決そのものではない、という点です。

Next.js のような環境では、実際のビルドや実行時の解決には Next.js やバンドラー側の仕組みも関わります。そのため、TypeScript 側の解決ルールと実際のビルド環境の解決ルールが大きくずれていると、型チェックでは通るのにビルドで失敗する、またはその逆のような問題が起きることがあります。

moduleResolution: "node" はなぜ見直し対象なのか

現在指定している moduleResolution: "node" は、現在の TypeScript では node10 相当の古い解決方式で、主に CommonJS の require を前提にした時代の Node.js 向け解決方式です。

一方で、現在の Node.js では ESM と CommonJS が併存しており、package.jsonexportsimports なども考慮する必要があります。TypeScript の公式ドキュメントでも、現代の Node.js 向けには node16nodenext、バンドラー向けには bundler が選択肢として説明されています。

TypeScript 7.0 を見据えると、moduleResolution: "node" のままにしておくのではなく、プロジェクトの実行環境に合わた値へ移行する必要があります。

Next.js アプリ本体では bundler が候補になる

Next.js のアプリケーションコードは、TypeScript の出力を Node.js がそのまま直接実行するというより、Next.js のビルドパイプラインを通して処理されます。

Next.js は TypeScript を組み込みでサポートしており、next devnext build の実行時に Next.js 側の仕組みも関わります。公式ドキュメントでも、Next.js は TypeScript を組み込みでサポートし、推奨される設定を含む tsconfig.json を扱うことが説明されています。

そのため、Next.js のアプリケーション本体では、moduleResolution: "bundler" が候補になります。

今回のプロジェクトでも、対象は Next.js のアプリケーションコードです。そのため、moduleResolution: "node" から moduleResolution: "bundler" へ変更する方針にしました。

修正後の tsconfig.json

{
  "compilerOptions": {
    "allowJs": true,
    "allowSyntheticDefaultImports": true,
    "module": "esnext",
    "moduleResolution": "bundler",
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "preserveConstEnums": true,
    "removeComments": false,
    "sourceMap": true,
    "strict": false,
    "strictPropertyInitialization": false,
    "strictNullChecks": false,
    "target": "esnext",
    "forceConsistentCasingInFileNames": true,
    "esModuleInterop": true,
    "isolatedModules": true,
    "resolveJsonModule": true,
    "noFallthroughCasesInSwitch": true
  }
}

変更したのは、次の1行です。

- "moduleResolution": "node",
+ "moduleResolution": "bundler",

今回は moduleResolution の見直しを主目的にしているため、他の設定はそのままにしています。

他にも見直したい設定

今回の主題は moduleResolution ですが、TypeScript 6.0 / 7.0 への移行を見据えると、他にも見直したい設定があります。

たとえば、今回の tsconfig.json では以下の設定が気になります。

"strict": false,
"strictNullChecks": false,
"strictPropertyInitialization": false

これらは TypeScript 7.0 で直ちに使えなくなる設定ではありません。

ただし、型チェックをかなり緩める設定です。現時点では既存コードへの影響を考慮して false のままにしていますが、今後の変更にあわせて段階的に見直していきたいです。

また、今回の tsconfig.json には含まれていませんが、baseUrlpaths を使っている場合も注意が必要です。特に import エイリアスを使っているプロジェクトでは、TypeScript 側の解決と Next.js 側の解決が一致しているかを確認しておく必要があります。

補足: TypeScript 7.0 について

TypeScript 7.0 では、TypeScript コンパイラや関連ツールの実装が Go ベースのネイティブ実装に移行します。

プレビュー段階では @typescript/native-preview パッケージとして提供されており、tsc の代わりに tsgo コマンドで試すことができます。

今回の moduleResolution: "node" の見直しは、この TypeScript 7.0 移行の流れとも関係しています。TypeScript 6.0 では node / node10 が非推奨となり、TypeScript 7.0 ではサポート対象外になる方針が示されているためです。

そのため、今すぐ TypeScript 7.0 へ移行するわけではなくても、既存の tsconfig.json を見直すきっかけになります。

Announcing TypeScript 7.0 Beta - TypeScript
Today we are absolutely thrilled to announce the release of TypeScript 7.0 Beta! If you haven’t been following TypeScript 7.0’s development, this release is significant in that it is built on a completely new foundation. Over the past year, we have been porting the existing TypeScript codebase from …

まとめ

今回は、Next.js プロジェクトで表示された moduleResolution: "node" のエラーをきっかけに、tsconfig.json を見直しました。

moduleResolution: "node" は、現在では node10 相当の古い解決方式として扱われます。TypeScript 7.0 を見据える場合は、nodenext または bundler への移行を検討する必要があります。

Next.js のアプリケーションコードは、通常 Next.js のビルドパイプラインを通して処理されます。そのため、今回のプロジェクトでは moduleResolution: "bundler" を候補にしました。

一方で、Node.js で直接実行するスクリプトやライブラリ公開用のコードでは、nodenext の方が適している場合があります。

bundler は「Next.js アプリ本体向けの選択肢」として捉え、プロジェクト内のコードの実行環境に応じて使い分けるのがよさそうです。