本文 AI 產出,尚未審核

TypeScript

編譯與建構工具整合 ── CI/CD 中的型別檢查


簡介

在現代前端開發中,TypeScript 已成為主流語言,因為它能在開發階段即捕捉大量潛在錯誤,提升程式碼可維護性。將型別檢查納入 CI/CD 流程,更是確保每一次提交(commit)或合併(merge)都符合嚴格的類型安全規範,避免因為型別錯誤而導致的部署失敗或線上 bug。

如果在本地開發時只靠 IDE 的即時檢查,仍有可能因環境差異或忽略 --noEmitOnError 等設定,讓錯誤程式碼順利通過測試、進入部署階段。將 TypeScript 編譯 作為 CI 的第一道關卡,能自動化、統一地執行型別檢查,讓團隊在每次 Pull Request(PR)時即得到回饋,降低回歸風險。

本篇文章將從 概念、工具整合、常見陷阱與最佳實踐 三個層面,說明如何在 CI/CD pipeline 中加入型別檢查,並提供完整的程式碼範例,讓初學者到中階開發者都能快速上手。


核心概念

1. 為什麼要在 CI 中執行 TypeScript 編譯

  • 一致性:CI 環境是唯一可控的執行環境,確保所有開發者使用相同的 TypeScript 版本與編譯設定。
  • 早期失敗tsc --noEmitOnError 會在型別錯誤時直接停止編譯,讓錯誤在 CI 階段即被截斷,避免不良程式碼流入 production。
  • 與測試、Lint 結合:型別錯誤往往是 lint 或單元測試無法捕捉的問題,將它作為獨立步驟,可與 Jest、ESLint 並行或順序執行。

2. TypeScript 編譯設定(tsconfig.json

以下是一個適合 CI 使用的 最小化 tsconfig.json,重點在於 嚴格模式不產出檔案(只檢查):

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "strict": true,                     // 開啟所有嚴格檢查
    "noEmit": true,                     // CI 只做型別檢查,不產出 .js
    "skipLibCheck": true,               // 加速編譯,忽略 .d.ts 內部檢查
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*.ts", "src/**/*.tsx"],
  "exclude": ["node_modules", "dist"]
}

Tip:在本地開發時,你可能會把 noEmit 改成 falsetsc 同時產出 JavaScript;但在 CI 中 建議 保持 noEmit:true,讓編譯僅作為型別檢查的工具。

3. 在 package.json 中加入檢查腳本

將型別檢查抽成獨立 NPM script,方便在 CI、IDE、或手動執行:

{
  "scripts": {
    "lint": "eslint src/**/*.ts",
    "type-check": "tsc --noEmit",          // 只檢查,不產出
    "build": "vite build",                 // 產出前端資源
    "test": "jest",
    "ci": "npm run lint && npm run type-check && npm run test && npm run build"
  }
}

Notenpm run ci 會依序執行 Lint → 型別檢查 → 單元測試 → 打包,任一步失敗即中斷,保證只有「全通」的提交才能進入部署階段。

4. CI 工作流範例(GitHub Actions)

以下是一個完整的 GitHub Actions 工作流範例,展示如何在每次 PR 或 push 時執行 TypeScript 型別檢查:

name: CI

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  build:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [18.x]

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'npm'

      - name: Install dependencies
        run: npm ci   # 使用 lockfile,確保版本一致

      - name: Run Lint
        run: npm run lint

      - name: TypeScript type check
        run: npm run type-check
        # 若型別錯誤,此步驟會失敗,整個 job 中止

      - name: Run unit tests
        run: npm test -- --coverage

      - name: Build production bundle
        run: npm run build

關鍵點

  • npm ci 會根據 package-lock.json 完全復現依賴,避免因套件版本差異導致型別檢查結果不同。
  • type-check 步驟使用 npm run type-check,若 tsc 回傳非 0 代碼,GitHub Actions 會直接標記 失敗,不會執行後續步驟。

5. 與建構工具(Webpack / Vite)結合

即使 tsc 已完成型別檢查,實際打包仍需要 loader 轉譯 TypeScript。以下分別示範 WebpackVite 的設定,確保 型別檢查bundle 兩者不相衝突:

5.1 Webpack + ts-loader(只做轉譯)

// webpack.config.js
const path = require('path');

module.exports = {
  entry: './src/index.ts',
  mode: 'production',
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: [
          {
            loader: 'ts-loader',
            options: {
              transpileOnly: true   // 只轉譯,不做型別檢查(已在 CI 完成)
            }
          }
        ],
        exclude: /node_modules/,
      },
    ],
  },
  resolve: {
    extensions: ['.ts', '.tsx', '.js'],
  },
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },
};

說明transpileOnly:truets-loader 僅執行語法轉譯,省去重複的型別檢查時間。若在本地想即時看到型別錯誤,可把此選項關掉或使用 fork-ts-checker-webpack-plugin

5.2 Vite(內建 ESBuild)

Vite 預設使用 esbuild 進行 TypeScript 轉譯,速度極快,但不執行完整型別檢查。只要在 CI 中跑 npm run type-check,即可確保安全:

// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  build: {
    outDir: 'dist',
    rollupOptions: {
      input: './src/main.tsx',
    },
  },
});

Tip:若想在本地開發時即時看到完整型別錯誤,可安裝 vite-plugin-checker

npm i -D vite-plugin-checker
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import checker from 'vite-plugin-checker';

export default defineConfig({
  plugins: [
    react(),
    checker({ typescript: true }) // 於開發伺服器中執行 tsc
  ],
});

常見陷阱與最佳實踐

陷阱 為何會發生 解決方式
CI 中 tsc 仍產出檔案 tsconfig.json 設定了 noEmit:false 或未加 --noEmit 參數 在 CI script 中明確使用 npm run type-checktsc --noEmit),或在 tsconfig.json "noEmit": true
型別檢查與 lint 同時失敗,訊息混雜 同一步驟同時執行 eslint + tsc,導致 log 雜亂 把 lint、type-check 分成不同 npm script,CI 中分別執行,讓失敗原因一目了然
依賴版本不一致導致型別錯誤 開發者本機使用較新/舊的 @types/*,CI 使用 lockfile 中的版本 在 CI 使用 npm ci,在本機同樣使用 npm cinpm installnpm shrinkwrap,保持依賴一致
transpileOnly:true 造成未捕捉錯誤 只在開發時使用 transpileOnly,忘記在 CI 中執行完整檢查 永遠 在 CI 中執行完整 tsc,即使本地開發使用 transpileOnly 以提升速度
忽略 skipLibCheck 帶來的隱藏錯誤 大型專案中 skipLibCheck:true 會跳過第三方 .d.ts 檢查,可能隱藏相容性問題 僅在 CI 中開啟 skipLibCheck:false 進行全檢,或在本地開發階段偶爾手動跑 tsc --skipLibCheck false 以驗證

最佳實踐

  1. 分離型別檢查與編譯:在 CI 中使用 tsc --noEmit,在建置階段使用 bundler(Webpack/Vite)僅做轉譯。
  2. 保持 TypeScript 版本一致:在 package.json 中明確指定 typescript 版本,並在 CI 中使用 npm ci
  3. 快取依賴與編譯結果:GitHub Actions 支援 actions/setup-nodecache: npm,能大幅縮短 CI 時間。
  4. 在 PR 評審時顯示型別錯誤:使用 GitHub Checks API 或 GitLab 的 code_quality 報告,把 tsc 的輸出轉成可點擊的檔案/行號。
  5. 把型別錯誤視為阻斷條件:在 CI 設定 continue-on-error: false(預設),確保任何型別錯誤都會導致 pipeline 失敗。

實際應用場景

場景 1:大型單頁應用(SPA)

  • 環境:React + Vite + Jest
  • 需求:每次 PR 必須通過 lint、型別檢查、單元測試、E2E 測試才可部署至 staging。
  • 解法:在 GitHub Actions 中設定四個獨立 job,使用 needs:type-check 為第一關卡,失敗即阻止後續 teste2edeploy

場景 2:微服務後端(Node.js + Express)

  • 環境:NestJS(內建 TypeScript) + Docker + GitLab CI
  • 需求:Docker image 必須在建構前保證所有 TypeScript 檔案無型別錯誤,且產出的 .js 必須與 tsc 的輸出一致。
  • 解法:在 .gitlab-ci.yml 中加入 tsc --noEmit 步驟,若成功再執行 docker build,並在 Dockerfile 中使用 COPY dist ./dist,確保產出的 JavaScript 只來自成功編譯的結果。

場景 3:開放原始碼套件(library)

  • 環境:Rollup + TypeScript + GitHub Actions
  • 需求:發布至 npm 前必須保證 d.ts 宣告檔正確、無任何未解決的型別錯誤。
  • 解法:在 CI 中使用 tsc --emitDeclarationOnly 產出宣告檔,並把 npm pack 前的產出與 package.json 中的 files 設定比對,確保宣告檔與實際程式碼同步。

總結

在 CI/CD 流程中加入 TypeScript 型別檢查,不只是提升程式碼品質,更是一種自動化的防護機制,能在每一次提交時即時阻止潛在錯誤流向生產環境。本文重點回顧如下:

  1. 設定嚴格的 tsconfig.jsonstrict:truenoEmit:true),讓編譯僅作為型別檢查。
  2. 把型別檢查抽成獨立 npm script,配合 lint、測試、打包形成完整的 CI pipeline。
  3. 在 CI 工作流(GitHub Actions / GitLab CI)中明確執行 tsc --noEmit,失敗即中斷後續步驟。
  4. 與建構工具(Webpack、Vite)結合 時,使用 transpileOnlyesbuild 只做語法轉譯,避免重複型別檢查。
  5. 避免常見陷阱:依賴版本不一致、混合執行 lint+type-check、忘記在 CI 中關閉 noEmit
  6. 最佳實踐:快取依賴、分離檢查與編譯、在 PR 中顯示型別錯誤、把型別錯誤視為阻斷條件。

透過上述步驟,你的專案將在 持續整合 階段就完成最嚴格的型別安全檢查,讓團隊更專注於功能開發,減少因型別錯誤導致的緊急修復與回滾。祝你在 TypeScript 與 CI/CD 的整合旅程中,寫出更安全、更穩定的程式碼!