TypeScript のパスエイリアスを使ったときに「Cannot find package '@/services' imported from ...」エラーが出る

# パスエイリアスとは

パスエイリアスとは、TypeScript において import 文のパスを短く可読性の高い文字列に置き換える機能のことです。例を見たほうがわかりやすいと思います。

Before
```typescript
import { UserService } from '../../../services/UserService';
```

After
```typescript
import { UserService } from '@/services/UserService';
```

ダラダラとした相対パス (`../../../`) を使わないため、コードの可読性や保守性が向上します。これは tsconfig.json に `paths` というのを書くことで実現できます。例えば以下のように書きます。

```javascript
{
    "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
```

発生していた行はここです。

```typescript
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` をインストールします。

```bash
npm install --save-dev tsc-alias
```

お好みで package.json のビルドスクリプトに組み込みます。

```json:package.json
{
    ...
    "scripts": {
        "build": "tsc && tsc-alias"
    },
    ...
}
```

ビルドを実行します。これだけでビルド後のスクリプトのパスエイリアスが相対パスに置き換えられます。

```bash
npm run build
```

## module-alias を使った解決方法

`module-alias` を使った解決方法もありました。
こちらはビルドスクリプトをいじらなくても良いのですが、その他の設定がやや複雑です。

### インストール

まず `module-alias` をプロジェクトにインストールします。

```bash
npm install --save-dev module-alias
```

次に、`package.json` に `_moduleAliases` というセクションを追加し、パスエイリアスを定義します。値はコンパイル後のディレクトリに向ける必要があります。例えば以下のように書きます。

```json
{
  ...(省略)...
  "_moduleAliases": {
    "@": "./dist"
  }
}
```

ここは `tsconfig.json` の `paths` 設定に合わせて調整してください。

### エントリーポイントの修正

Node.js のエントリーポイントとなるファイル (通常は `index.ts` や `server.ts` など) の**先頭で** `module-alias` を import します。

```typescript
import 'module-alias/register.js'
```

### ビルドして実行

お好みで package.json にビルドスクリプトを追加します。

```json:package.json
{
    ...
    "scripts": {
        "build": "tsc"
    },
    ...
}
```

ビルドします。

```bash
npm run build
```

これでビルド後のスクリプトを実行すると、Node.js が起動時に `package.json` のエイリアス設定を読み込み、`@/services` のようなパスエイリアスを正しく解決できるようになります。

個人的には、パスエイリアスの設定を二重で持たなくてはいけない点、エントリーポイントに import を足すのが気持ち悪くて、あまり良いと思えませんでした。

## まとめ

TypeScript でパスエイリアスを利用すると、コードの可読性と保守性が向上しますが、`tsc` 単体ではコンパイル後の JavaScript ファイルでエイリアスが解決されません。追加で `tsc-alias` や `module-alias` などのライブラリを利用することで「Cannot find package」エラーを回避することができます。