できない.dev

TypeScript の tsconfig.json paths が実行時に解決できない

tsc は paths を「型解決」専用に使い、コンパイル後の JS にエイリアスを書き換えない。
Node 直接実行や ts-node では tsconfig-paths などのローダ、bundler では別途 alias 設定が必要。

公開: 更新:

要約

tsconfig.jsonpathstsc が型を解決するためだけのマッピング で、コンパイル後の JS は import "@/foo" のまま残る。
Node がそのパスを知らなければ実行時に Cannot find module になる。
実行系(ts-node / node / bundler)ごとに 同じ alias を別途設定する 必要がある。

よくある原因

  1. paths は型専用: tsc 単体ではコンパイル時にパス書き換えをしない(公式の明記(新しいタブで開く)
  2. ts-node でローダ未指定: そのままでは paths を解釈できず、@/foonode_modules で探して失敗する
  3. bundler 側未設定: webpack / Vite / esbuild はそれぞれ独自の alias を持ち、tsconfig の paths を自動採用しない
  4. 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.ts

tsconfig-paths/registerpaths を読んで 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.jsCannot 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 時点の出力

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