本文 AI 產出,尚未審核

TypeScript 實務應用:Node.js + TypeScript 的模組解析策略(moduleResolution)


簡介

在使用 TypeScript 開發 Node.js 應用時,模組解析(module resolution) 是編譯器決定如何從 import / require 語句找到實際檔案的核心機制。若解析策略設定不當,常會出現找不到模組、路徑錯誤或編譯時間過長等問題,直接影響開發效率與程式的可維護性。

本篇文章針對 moduleResolution 這個 tsconfig.json 中的選項進行深入說明,從概念、常見設定、實作範例,到實務上的最佳實踐與陷阱,幫助你在 Node.js + TypeScript 專案中快速、正確地管理模組路徑。


核心概念

1. 為什麼需要模組解析策略?

TypeScript 必須在編譯階段把 import / export 轉換成 Node.js 能執行的 require 或 ES 模組語法。這個過程涉及兩件事:

  1. 找出檔案位置(相對路徑、絕對路徑或套件名稱)。
  2. 決定使用哪種檔案類型.ts.tsx.d.ts.js.json 等)。

不同的解析策略會影響上述兩項行為的細節。

2. 兩大解析模式:classic vs node

模式 主要特徵 適用情境
classic 舊版 TypeScript 的預設行為,類似於 JavaScript 的相對路徑解析,僅支援 ./../ 及裸檔名(不會自動搜尋 node_modules 老舊專案或需要自行實作模組搜尋邏輯的情況
node 依照 Node.js 的模組解析規則:先找相對/絕對路徑,找不到時會搜尋 node_modulespackage.json 中的 mainexports 大多數 Node.js + TypeScript 專案的首選

:在 tsconfig.json 中未指定 moduleResolution 時,若 modulecommonjsesnext 等,編譯器會自動使用 node;若 moduleamdsystem,則使用 classic

3. 影響解析的其他設定

設定項目 作用說明
baseUrl 設定非相對路徑的基礎目錄,讓 import "utils" 能從指定目錄開始搜尋。
paths 為特定路徑別名(alias)提供映射表,常與 baseUrl 搭配使用。
rootDirs 多根目錄的合併視圖,讓相同相對路徑在不同根目錄下的檔案互相參照。
typeRoots / types 控制全域型別宣告的搜尋範圍,間接影響模組解析。

程式碼範例

以下示範在 Node.js + TypeScript 專案中,如何透過 tsconfig.json 正確設定模組解析,以及常見的使用情境。

1️⃣ 基本 node 解析(最常見)

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "commonjs",
    "moduleResolution": "node",   // 明確指定 node 解析
    "outDir": "./dist",
    "strict": true
  },
  "include": ["src"]
}
// src/server.ts
import express from "express";          // 會自動在 node_modules 中找 express
import { Router } from "./router";      // 相對路徑解析

const app = express();
app.use("/api", Router);
app.listen(3000, () => console.log("Server running"));

說明express 透過 node_modules/express/package.json 中的 main 欄位指向 express.js,編譯器即會正確解析。

2️⃣ 使用 baseUrl + paths 建立別名

// tsconfig.json
{
  "compilerOptions": {
    "module": "commonjs",
    "moduleResolution": "node",
    "baseUrl": "./src",                // 所有非相對路徑都從 src 開始
    "paths": {
      "@utils/*": ["utils/*"],          // @utils 會對映到 src/utils
      "@models/*": ["models/*"]
    }
  }
}
// src/controllers/userController.ts
import { hashPassword } from "@utils/crypto";   // 由別名解析到 src/utils/crypto.ts
import { User } from "@models/user";

export async function createUser(name: string, pw: string) {
  const hashed = await hashPassword(pw);
  return new User(name, hashed);
}

技巧:別忘了在 webpackts-nodeESM 執行環境中同步設定相同的別名(例如 webpack.config.jsresolve.alias),否則執行時會找不到模組。

3️⃣ classic 解析的特殊情況

// tsconfig.json
{
  "compilerOptions": {
    "module": "amd",
    "moduleResolution": "classic"
  }
}
// src/legacy/moduleA.ts
export const hello = "world";

// src/main.ts
import { hello } from "moduleA";   // 只會在相同目錄或相對路徑下搜尋

重點:在 classic 模式下,若使用裸模組名稱(如上例 moduleA),編譯器不會去 node_modules,必須自行確保檔案在相對路徑可被找到,否則會產生 TS2307: Cannot find module 錯誤。

4️⃣ 同時使用 rootDirs 處理 monorepo

// tsconfig.json(位於 monorepo 根目錄)
{
  "compilerOptions": {
    "module": "commonjs",
    "moduleResolution": "node",
    "baseUrl": ".",
    "rootDirs": ["packages/api/src", "packages/shared/src"]
  }
}
// packages/api/src/service.ts
import { Logger } from "shared/logger";   // 透過 rootDirs,"shared/logger" 會映射到 packages/shared/src/logger.ts

說明rootDirs 讓不同子專案之間的相對路徑保持一致,避免因實際檔案位置不同而產生錯誤。

5️⃣ paths 搭配 typeRoots 解決型別套件

// tsconfig.json
{
  "compilerOptions": {
    "moduleResolution": "node",
    "baseUrl": "./src",
    "paths": {
      "@config": ["config/index"]
    },
    "typeRoots": ["./node_modules/@types", "./src/types"]
  }
}
// src/config/index.ts
export const PORT = 3000;

// src/app.ts
import { PORT } from "@config";   // 別名解析

提示:若 @config 所指的模組同時有自訂型別(.d.ts),請確保 typeRoots 包含其路徑,否則編譯器會顯示 implicit any 警告。


常見陷阱與最佳實踐

陷阱 可能的症狀 解決方式
忘記設定 moduleResolution: "node" commonjs 專案中,import "lodash" 失敗或找不到 node_modules 明確在 tsconfig.json 加入 moduleResolution: "node",或確保 modulecommonjs/esnext 時自動使用
別名與執行環境不同步 開發時編譯通過,執行時 Cannot find module '@utils/...' ts-node, Webpack, ESM 等工具中,同步設定相同的 alias / paths
相對路徑與 baseUrl 混用導致重複 同一檔案被編譯兩次,產生多個 .js 盡量統一使用 baseUrl + paths,或限制相對路徑只用於同一層級以下
paths 中使用 * 卻未對應 TS6053: File '/src/utils' not found 確保 * 前後的通配符對應正確,例如 "@utils/*": ["utils/*"]
Monorepo 中 rootDirs 設定不完整 交叉引用失敗、編譯錯誤 TS2307 在根 tsconfig.json 中列出所有子專案的 src 目錄,或使用 projectReferences 結合 composite 設定

最佳實踐

  1. 統一使用 node 解析:除非有特殊需求,建議在 Node.js 專案中將 moduleResolution 設為 "node",保持與執行環境一致。
  2. baseUrl + paths 建立別名:減少長相對路徑 (../../../) 的可讀性問題,並在 IDE 中自動補全。
  3. 在 CI/CD 中驗證別名:使用 tsc --noEmitnpm run lint 確認所有別名在編譯階段皆能正確解析。
  4. 結合 projectReferences:大型 monorepo 建議使用 TypeScript 3.0+ 的 references 功能,讓每個子套件都有自己的 tsconfig.json,同時在根目錄設定 moduleResolution: "node"
  5. 同步工具設定:Webpack、Rollup、Vite、ts-node、nodemon 等工具皆需要對應的別名設定,否則會在執行時拋出 MODULE_NOT_FOUND

實際應用場景

1️⃣ 建立大型後端服務的模組別名

在一個以 Clean Architecture 為核心的微服務中,會有 src/domain, src/application, src/infrastructure 等層級。若直接使用相對路徑:

import { UserRepository } from "../../../infrastructure/repositories/userRepository";

會讓程式碼變得雜亂且易出錯。透過 paths

{
  "compilerOptions": {
    "baseUrl": "./src",
    "paths": {
      "@domain/*": ["domain/*"],
      "@app/*": ["application/*"],
      "@infra/*": ["infrastructure/*"]
    }
  }
}

即可寫成:

import { UserRepository } from "@infra/repositories/userRepository";

2️⃣ 多語系前端/後端共用型別

在同一 monorepo 中,前端(React)與後端(Node)共享 src/types/api.d.ts。使用 rootDirs

{
  "compilerOptions": {
    "rootDirs": ["packages/frontend/src", "packages/backend/src"]
  }
}

讓兩側都能以相同的相對路徑引用:

import { ApiResponse } from "shared/types/api";

3️⃣ 以 moduleResolution: classic 處理舊版 AMD 模組

某些遺留系統仍使用 AMD(RequireJS)載入模組,且模組名稱已寫死在 HTML 中。此時必須保留 classic 解析,並在 tsconfig.json 明確指定:

{
  "compilerOptions": {
    "module": "amd",
    "moduleResolution": "classic"
  }
}

配合 paths 可把舊有的裸模組名稱映射到新位置:

{
  "paths": {
    "legacy/util": ["legacy/src/util"]
  }
}

總結

  • moduleResolution 決定了 TypeScript 如何在編譯階段定位模組檔案,node 為大多數 Node.js + TypeScript 專案的首選。
  • 配合 baseUrlpathsrootDirs 等設定,我們可以建立乾淨、可維護的模組別名,避免長相對路徑帶來的閱讀與維護負擔。
  • 常見的陷阱包括別名未同步至執行環境、classic 解析導致的模組找不到,以及在 monorepo 中 rootDirs 設定不完整。透過 最佳實踐(統一解析模式、同步工具設定、CI 驗證)可以有效降低這些風險。
  • 在實務上,無論是大型微服務、前後端共用型別,或是遺留的 AMD 系統,正確的模組解析策略都是保持專案可擴展、開發效率的關鍵。

掌握了 moduleResolution 與相關的 tsconfig 設定後,你就能在 Node.js + TypeScript 的開發旅程中更加得心應手,快速構建高品質、易維護的程式碼基礎。祝你寫程式愉快! 🚀