パスエイリアスとは
パスエイリアスとは、TypeScript において import 文のパスを短く可読性の高い文字列に置き換える機能のことです。例を見たほうがわかりやすいと思います。
Before
import { UserService } from '../../../services/UserService';
After
import { UserService } from '@/services/UserService';
ダラダラとした相対パス (../../../
) を使わないため、コードの可読性や保守性が向上します。これは tsconfig.json に paths
というのを書くことで実現できます。例えば以下のように書きます。
{
"compilerOptions": {
"target": "es2020",
"module": "NodeNext",
"moduleResolution": "nodenext",
"rootDir": "./src",
"outDir": "./dist",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"sourceMap": true,
"paths": {
"@/*": ["./src/*"] // ←ここ
}
},
"include": [
"./src/**/*.ts"
]
}
"@/*": ["./src/*"]
のように指定すると、例えば @/hoge
と書いたときにルートディレクトリから ./src/hoge
と指定したと同じ意味になります。
これはとても便利ですし、だいぶ import がすっきり書けました。
「Cannot find package '@/services' imported from ...」エラー
しかしこれを tsc
でビルドして実行しようとしたところ、エラーが発生しました。
Error [ERR_MODULE_NOT_FOUND]: Cannot find package '@/services' imported from /workspace/dist/controllers/apiController.js
発生していた行はここです。
import { UserService } from '@/services/UserService';
このエラーは TypeScript の tsconfig.json
で設定したパスエイリアスが、コンパイル後の JavaScript ファイルでは解決されず、Node.js がそのパスを認識できないために発生します。
ちなみにローカルなどで tsx
を使って簡易的に Node.js を実行している限りはエラーは出ません。tsx
は上手いことパスエイリアスを解決してくれていたのです。
エラーの原因詳細
TypeScript コンパイラ (tsc
) は、tsconfig.json
の paths
オプションを型チェックやコード補完には利用しますが、デフォルトではコンパイル後の JavaScript ファイルの import パスを自動的に書き換える機能はありません。
そのため、コンパイル後の JavaScript ファイルには、パスエイリアス (@/services
) がそのまま残ってしまい、Node.js がこのパスを標準のモジュール解決アルゴリズムで探しても見つからないため、ERR_MODULE_NOT_FOUND
エラーが発生します。
なお、TypeScript のビルドに Vite や Webpack を使っている場合は、それらのツールがパスエイリアスを解決してくれるのであまり気にしなくても良いと思います。
自分は Express で API 開発をしていたので「Vite はフロントエンド用だからちょっと違うし・・・」「Webpack はちょっと設定めんどくさいし・・・」と思ってもっと手軽な方法を探してみました。
いくつか解決方法があります。
tsc-alias を使った解決方法
最もお手軽だと思ったのが tsc-alias
を使った解決方法です。
まず tsc-alias
をインストールします。
npm install --save-dev tsc-alias
お好みで package.json のビルドスクリプトに組み込みます。
{
...
"scripts": {
"build": "tsc && tsc-alias"
},
...
}
ビルドを実行します。これだけでビルド後のスクリプトのパスエイリアスが相対パスに置き換えられます。
npm run build
module-alias を使った解決方法
module-alias
を使った解決方法もありました。
こちらはビルドスクリプトをいじらなくても良いのですが、その他の設定がやや複雑です。
インストール
まず module-alias
をプロジェクトにインストールします。
npm install --save-dev module-alias
次に、package.json
に _moduleAliases
というセクションを追加し、パスエイリアスを定義します。値はコンパイル後のディレクトリに向ける必要があります。例えば以下のように書きます。
{
...(省略)...
"_moduleAliases": {
"@": "./dist"
}
}
ここは tsconfig.json
の paths
設定に合わせて調整してください。
エントリーポイントの修正
Node.js のエントリーポイントとなるファイル (通常は index.ts
や server.ts
など) の先頭で module-alias
を import します。
import 'module-alias/register.js'
ビルドして実行
お好みで package.json にビルドスクリプトを追加します。
{
...
"scripts": {
"build": "tsc"
},
...
}
ビルドします。
npm run build
これでビルド後のスクリプトを実行すると、Node.js が起動時に package.json
のエイリアス設定を読み込み、@/services
のようなパスエイリアスを正しく解決できるようになります。
個人的には、パスエイリアスの設定を二重で持たなくてはいけない点、エントリーポイントに import を足すのが気持ち悪くて、あまり良いと思えませんでした。
まとめ
TypeScript でパスエイリアスを利用すると、コードの可読性と保守性が向上しますが、tsc
単体ではコンパイル後の JavaScript ファイルでエイリアスが解決されません。追加で tsc-alias
や module-alias
などのライブラリを利用することで「Cannot find package」エラーを回避することができます。