本文 AI 產出,尚未審核

TypeScript 編譯與設定:compilerOptionsoutDirrootDir


簡介

在使用 TypeScript 開發大型專案時,編譯後的檔案結構往往直接影響到後續的部署、測試與維護成本。tsconfig.json 中的 compilerOptions 提供了許多控制編譯行為的參數,其中最常用、也是最容易產生誤解的就是 outDirrootDir

  • outDir(output directory)決定 編譯後的 JavaScript、宣告檔 (.d.ts) 以及 source map 要寫入的目錄。
  • rootDir(root directory)則告訴編譯器 原始的 TypeScript 檔案根位置,讓編譯器在產出目錄時能正確保留相對路徑。

如果這兩個設定不當,常會出現「檔案找不到」或「路徑錯亂」的問題,導致開發流程被迫中斷。本文將深入說明 outDir / rootDir 的作用原理、實作範例、常見陷阱與最佳實踐,幫助你在專案中建立 乾淨、可預測的編譯輸出


核心概念

1. rootDir:告訴編譯器「從哪裡開始找」

rootDir 只是一個 參考點,編譯器會以它為基礎,計算每個檔案相對於根目錄的路徑,之後再把相同的相對路徑套用到 outDir。如果未明確設定,TypeScript 會自動推斷最深的共同祖先目錄,這在小型專案還好,大型或多層結構的專案很容易產生意外。

{
  "compilerOptions": {
    "rootDir": "src"
  }
}

範例說明src/app/main.tssrc/utils/helper.ts 兩個檔案相對於 src 的路徑分別是 app/main.tsutils/helper.ts

2. outDir:決定編譯產出放哪裡

outDir 必須是一個 相對或絕對路徑,編譯器會把所有轉譯後的檔案寫入此目錄,並保持 rootDir 計算出的相對結構。

{
  "compilerOptions": {
    "outDir": "dist"
  }
}

結果src/app/main.ts 會被編譯成 dist/app/main.jssrc/utils/helper.ts 會變成 dist/utils/helper.js

3. rootDir + outDir 的互動流程

  1. 掃描 include / files → 找到所有符合條件的 .ts.tsx 檔案。
  2. 根據 rootDir 計算相對路徑(若未設定,使用最深的共同父目錄)。
  3. 將相對路徑套用到 outDir,產生最終輸出路徑。
  4. 寫入檔案(包括 .js.d.ts.js.map 依設定)。

圖示
src/ (rootDir) → dist/ (outDir)
src/models/user.tsdist/models/user.js


程式碼範例

以下示範 5 個常見的設定與結果,讓你快速掌握實際應用。

範例 1:最簡單的 rootDir + outDir

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "rootDir": "src",
    "outDir": "build"
  }
}
# 檔案結構
src/
 ├─ index.ts
 └─ utils/
     └─ logger.ts

編譯後

build/
 ├─ index.js
 └─ utils/
     └─ logger.js

範例 2:未設定 rootDir,自動推斷的結果

{
  "compilerOptions": {
    "outDir": "dist"
  }
}
src/
 ├─ server/
 │   └─ app.ts
 └─ client/
     └─ main.ts

自動推斷:最深共同父目錄是 src,所以 dist/server/app.jsdist/client/main.js
src 內還有其他非 TypeScript 檔案(例如 .json),也會被一起搬移,可能造成不必要的檔案。

範例 3:使用多層 rootDir 結構

{
  "compilerOptions": {
    "rootDir": "src/modules",
    "outDir": "output"
  }
}
src/
 └─ modules/
     ├─ auth/
     │   └─ login.ts
     └─ data/
         └─ fetch.ts

編譯結果

output/
 ├─ auth/
 │   └─ login.js
 └─ data/
     └─ fetch.js

範例 4:配合 pathsbaseUrl 使用

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@utils/*": ["src/utils/*"]
    },
    "rootDir": "src",
    "outDir": "lib"
  }
}
// src/app.ts
import { format } from "@utils/formatter";

編譯後 lib/app.js 仍會保留相同的相對路徑,不會把別名直接寫入磁碟路徑,因此 paths 僅影響編譯階段的模組解析。

範例 5:搭配 declaration 產生型別宣告檔

{
  "compilerOptions": {
    "rootDir": "src",
    "outDir": "dist",
    "declaration": true,
    "emitDeclarationOnly": false
  }
}
src/
 └─ services/
     └─ api.ts   // 含有 export interface

編譯結果

dist/
 └─ services/
     ├─ api.js
     └─ api.d.ts   // 介面宣告同步產出

常見陷阱與最佳實踐

陷阱 可能的症狀 解決方式
未設定 rootDir,導致輸出路徑混亂 dist 內出現多層 srctest 目錄,甚至把 node_modules 複製進去。 明確指定 rootDir 為專案的程式碼根目錄(通常是 src)。
outDir 指向同一層目錄 編譯時會 覆寫原始 .ts 檔案,造成資料遺失。 確保 outDirrootDir 不相同,且通常放在 distbuildlib 等獨立資料夾。
使用絕對路徑但未加 . 前綴 在 Windows 與 Linux 上路徑分隔符不同,可能產生「找不到檔案」的錯誤。 使用 相對路徑"./dist")或在 tsconfig.json 中統一使用 POSIX 風格 /
rootDir 包含測試檔 測試檔會一併被編譯到 outDir,造成產出體積變大。 將測試檔放在 test__tests__,並在 exclude 中排除,或把 rootDir 設為 src
outDir.gitignore 忽略,卻誤提交 CI/CD 流程找不到編譯產出,部署失敗。 確認 .gitignore 只排除 暫存檔(如 dist/),而 CI 會在建置階段自行產出。

最佳實踐

  1. 固定 rootDirsrc,讓所有程式碼都在同一層級下管理。
  2. outDir 建議使用 distbuildlib,保持與原始碼分離。
  3. tsconfig.json 中加入 excludeinclude,明確告訴編譯器哪些檔案不需要編譯。
  4. 若專案需要 多個輸出目錄(例如 es5esnext),可以使用 extends 產生多個配置檔,避免一次設定過於複雜。
  5. 在 CI/CD pipeline 中加入 npm run clean && tsc,確保每次建置前先清除舊的 outDir,避免殘留檔案。

實際應用場景

1. 前端框架(React / Vue)結合 Webpack

  • 需求:原始碼放在 src/,Webpack 需要讀取已編譯的 JavaScript。
  • 做法:在 tsconfig.json 設定 rootDir: "src"outDir: "dist",Webpack 的 entry 指向 dist/index.js。這樣即使在開發模式下使用 ts-loader,產出的檔案仍保持一致的目錄結構,方便熱更新與 source map。

2. Node.js 套件發佈(npm package)

  • 需求:將 TypeScript 寫的套件編譯成 CommonJS 與 ES Module,並同時輸出型別宣告。
  • 做法
    {
      "compilerOptions": {
        "rootDir": "src",
        "outDir": "lib",
        "declaration": true,
        "module": "ESNext",
        "target": "ES2019"
      },
      "include": ["src/**/*.ts"]
    }
    
    • src 內的檔案會保持原始相對路徑,最終在 lib 中得到 index.jsindex.d.ts 等,npm publish 時只把 lib 包入。

3. 單元測試(Jest)與測試編譯

  • 需求:測試檔案放在 tests/,不希望它們被編譯到正式產出目錄。
  • 做法
    {
      "compilerOptions": {
        "rootDir": "src",
        "outDir": "dist"
      },
      "exclude": ["tests"]
    }
    
    • Jest 會使用 ts-jest 直接編譯測試檔,而 tsc 只負責產出正式程式碼。

4. 多語系資源檔(i18n)與自動生成

  • 需求:把 src/locales/*.ts 轉成 dist/locales/*.js,讓前端在執行時動態載入。
  • 做法:同樣利用 rootDir / outDir,保持 locales 目錄結構不變,讓程式碼只需要 import locale from "./locales/zh-TW"

總結

  • rootDir 定義 原始程式碼的根目錄,決定相對路徑的計算基準。
  • outDir 定義 編譯結果的輸出位置,並保留 rootDir 計算出的目錄結構。
  • 正確設定兩者可以避免 路徑錯亂、檔案遺失,提升 專案可維護性與部署一致性
  • 常見陷阱包括未設定 rootDiroutDirrootDir 同層、測試檔被編譯等,透過 明確的 exclude / include、分離的輸出資料夾 以及 CI 清理流程 可有效解決。
  • 無論是 前端框架、Node 套件、單元測試,或是 多語系資源outDir / rootDir 都是構建乾淨、可預測編譯產出的關鍵。

掌握了這兩個參數的使用,你就能在任何規模的 TypeScript 專案中,保持 清晰的檔案結構、快速的建置速度,並減少因路徑問題導致的除錯時間。祝你寫程式愉快,編譯順利! 🚀