できない.dev

TypeScript で `.js` 拡張子を付けないと NodeNext の import が解決できない

moduleResolution: node16 / nodenext では ESM 仕様に従い相対 import に拡張子が必須。
元ファイルが .ts でも import 側には .js と書くのが正解。
バンドラ前提なら bundler に切り替えるのが楽。

#typescript#esm#nodenext#module-resolution#import

公開:

要約

Relative import paths need explicit file extensions in ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext' というエラーは、ESM 仕様準拠の挙動を TypeScript がそのまま追従しているもの。import './foo'import './foo.js' に書き換えれば解決する。
違和感があるが、これが正しい。

よくある原因

  1. tsconfig.json"moduleResolution": "node16" または "nodenext" にしている
  2. package.json"type": "module" で完全 ESM プロジェクトになっている
  3. 旧来の moduleResolution: node の慣れで、拡張子なしの import を多用している
  4. パスエイリアス(@/foo)には警告が出ないので、相対 import だけ修正漏れしている

解決策

1. 相対 import に .js を付ける

// 修正前
import { foo } from "./foo";
import { bar } from "../utils/bar";
 
// 修正後
import { foo } from "./foo.js";
import { bar } from "../utils/bar.js";

元ファイルが .ts でも import パスは .js
TypeScript はコンパイル後のパスとしてこれを解釈する。
違和感があるが、Node.js の ESM 仕様が拡張子必須なのでそれに従う形になっている。
詳細は TypeScript Modules Reference 公式ドキュメント を参照。

2. バンドラ前提なら bundler に切り替える

Next.js / Vite / esbuild などバンドラがパス解決を担うプロジェクトでは:

{
  "compilerOptions": {
    "module": "esnext",
    "moduleResolution": "bundler"
  }
}

bundler は拡張子を要求しない。
フロントエンド寄りのプロジェクトはこちらの方が無難。
各値の選び方は tsconfig moduleResolution 公式 にまとまっている。

3. 検証専用なら .ts 拡張子を許す

noEmit: true で型検査のみのプロジェクトでは:

{
  "compilerOptions": {
    "moduleResolution": "node16",
    "allowImportingTsExtensions": true,
    "noEmit": true
  }
}

これで import "./foo.ts" のように .ts 拡張子を書けるようになる。
ただし noEmit 必須なので、ビルド成果物を出すプロジェクトでは使えない。

4. 既存コードの一括変換

VS Code の正規表現置換、または ts-add-js-extension のようなツールで from "./foo"from "./foo.js" の一括変換が可能。
ファイル数が多いプロジェクトの移行時に有効。

この記事は役立ちましたか?