環境
- nodejs v24
- typescript v5
まずはpathsとsubpath importsをそれぞれ押さえてみる。
paths
paths は baseUrl か tsconfig.json を基準に、TypeScript の import と require の解決先を再マップする機能だ。
ただしこれはあくまで TypeScript 側の解決規則であって、実行時の Node.js にそのまま伝わるわけではない。
設定例
// tsconfig.json { "compilerOptions": { "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", "rootDir": "./src", "outDir": "./dist", "baseUrl": ".", "paths": { "@/*": ["./src/*"] } }, "include": ["src/**/*.ts"] }
// src/lib/math.ts export function add(a: number, b: number) { return a + b; }
// src/index.ts import { add } from "@/lib/math"; console.log(add(1, 2));
- メリットは、TypeScript に対して短い import 名の解決先を宣言できるので、型チェック用のエイリアスとして扱いやすいことだ
- デメリットは、
tscは@/lib/mathのような specifier を出力時に直さないので、Node.js で動かすには bundler、loader、post-build 変換など別の仕組みが必要になることだ
subpath imports
Node.js の subpath imports は package.json の imports フィールドで定義する private mapping で、同じ package の内側からだけ使え、キーは必ず # で始まる。
TypeScript v5 では node16、nodenext、bundler の解決モードでこの imports を参照するから、Node と TypeScript の解決規則を package.json に寄せやすい。
設定例
// package.json { "name": "sample-app", "type": "module", "imports": { "#lib/*.js": "./dist/lib/*.js" }, "scripts": { "build": "tsc -p tsconfig.json", "start": "node dist/index.js" } }
// tsconfig.json { "compilerOptions": { "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", "rootDir": "./src", "outDir": "./dist", "resolvePackageJsonImports": true }, "include": ["src/**/*.ts"] }
// src/lib/math.ts export function add(a: number, b: number) { return a + b; }
// src/index.ts import { add } from "#lib/math.js"; console.log(add(1, 2));
- メリットは、Node.js 自身が
importsを理解するので、実行時の解決と TypeScript の解決を揃えやすいことだ - また
importsはexportsと同様のルールで解決され、条件分岐を使え、しかもexportsと違って外部 package へのマッピングも許されるのもメリット - デメリットは、package の外からは使えず、specifier が
#...形式に制限されることだ - さらに Node.js の
import解決は拡張子探索をせず、subpath pattern も特別な拡張子補完をしないから、#lib/math.jsのように.jsまで含めて書く前提で設計したほうが安全だ
テスト時のモッキングで差が出る
モックする際は subpath imports のほうが有利な場合がある。
paths を使っている場合、テスト時に依存を差し替えたいときに以下のような問題が起きる。
// src/services/calculator.ts import { add } from "@/lib/math"; export function calculate(x: number, y: number) { return add(x, y) * 2; }
テストで add をモックしようとしても、@/lib/math という文字列そのものを解決する必要があるため、テストランナーやローダーの設定が複雑になる。Jest なら moduleNameMapper、Vitest なら resolve.alias の設定が必要だ。
一方で subpath imports を使うと、imports フィールドに条件分岐を書ける。
// package.json { "imports": { "#lib/*.js": { "default": "./dist/lib/*.js", "test": "./dist/lib/__mocks__/*.js" } } }
このように設定すると、NODE_ENV=test などで実行したときは自動的に __mocks__ ディレクトリの実装を参照するようになる。TypeScript も同じ package.json を参照するため、型チェックと実行時の動作が自然に一致する。
// src/lib/__mocks__/math.ts export function add(a: number, b: number) { return 100; // テスト用の固定値 }
ビルドやバンドラーの設定をいじらずに、Node.js 標準の機能だけでモック切り替えが実現できるのは大きな利点だ。

