TypeScript の tsconfig.json paths が実行時に解決できない
tsc は paths を「型解決」専用に使い、コンパイル後の JS にエイリアスを書き換えない。
Node 直接実行や ts-node では tsconfig-paths などのローダ、bundler では別途 alias 設定が必要。
公開: 更新:
要約
tsconfig.json の paths は tsc が型を解決するためだけのマッピング で、コンパイル後の JS は import "@/foo" のまま残る。
Node がそのパスを知らなければ実行時に Cannot find module になる。
実行系(ts-node / node / bundler)ごとに 同じ alias を別途設定する 必要がある。
よくある原因
- paths は型専用: tsc 単体ではコンパイル時にパス書き換えをしない(公式の明記(新しいタブで開く))
- ts-node でローダ未指定: そのままでは paths を解釈できず、
@/fooを node_modules で探して失敗する - bundler 側未設定: webpack / Vite / esbuild はそれぞれ独自の
aliasを持ち、tsconfig の paths を自動採用しない - baseUrl 欠落:
pathsのキー解決にはbaseUrlが必要
解決策
1. tsconfig 側の最低構成
{
"compilerOptions": {
"baseUrl": "./src",
"paths": {
"@/*": ["*"]
}
}
}baseUrl が無いと paths のキーが解決されない。
2. ts-node で実行する
npm install -D ts-node tsconfig-paths
node -r ts-node/register -r tsconfig-paths/register src/main.tstsconfig-paths/register が paths を読んで Node の解決経路に合流させる。ts-node の代わりに tsx を使う場合も同様の前処理が必要。
3. tsc 出力を実体パスに書き換える
npm install -D tsc-alias
npx tsc && npx tsc-aliasビルド成果物の import "@/foo" を相対パスに置換する。
production 配布時はこちらが安全。
4. bundler の alias を合わせる
// vite.config.ts
import { defineConfig } from "vite";
import path from "node:path";
export default defineConfig({
resolve: {
alias: { "@": path.resolve(__dirname, "src") }
}
});webpack / esbuild も同様に tsconfig の paths と一致するマッピング を定義する。
実行例
実際に上記の手順を node:20(Debian GNU/Linux 12)環境で動かすと、tsc 単体のコンパイルでは出力 JS の require("@/greet") がそのまま残り、node dist/main.js は Cannot find module '@/greet'(終了コード 1)で失敗するが、続けて tsc-alias を適用すると同じ行が require("./greet") に書き換えられ、終了コード 0 で正常終了することが確認できる。
Version 5.9.3$ npx tsc
tsc 終了コード: 0$ grep -n "require(" dist/main.js
3:const greet_1 = require("@/greet");$ node dist/main.js
node:internal/modules/cjs/loader:1210
throw err;
^
Error: Cannot find module '@/greet'
Require stack:
- /tmp/tmp.zSxdQcyZAj/dist/main.js
at Module._resolveFilename (node:internal/modules/cjs/loader:1207:15)
at Module._load (node:internal/modules/cjs/loader:1038:27)
at Module.require (node:internal/modules/cjs/loader:1289:19)
at require (node:internal/modules/helpers:182:18)
at Object.<anonymous> (/tmp/tmp.zSxdQcyZAj/dist/main.js:3:17)
at Module._compile (node:internal/modules/cjs/loader:1521:14)
at Module._extensions..js (node:internal/modules/cjs/loader:1623:10)
at Module.load (node:internal/modules/cjs/loader:1266:32)
at Module._load (node:internal/modules/cjs/loader:1091:12)
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:164:12) {
code: 'MODULE_NOT_FOUND',
requireStack: [ '/tmp/tmp.zSxdQcyZAj/dist/main.js' ]
}
Node.js v20.20.2
node 終了コード: 1$ npx tsc-alias$ grep -n "require(" dist/main.js
3:const greet_1 = require("./greet");$ node dist/main.js
Hello, world
node 終了コード: 0(0 なら実体パスに解決できている)— 2026-08-02 時点の出力