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改成false讓tsc同時產出 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"
}
}
Note:
npm 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。以下分別示範 Webpack 與 Vite 的設定,確保 型別檢查 與 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:true讓ts-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-check(tsc --noEmit),或在 tsconfig.json 加 "noEmit": true |
| 型別檢查與 lint 同時失敗,訊息混雜 | 同一步驟同時執行 eslint + tsc,導致 log 雜亂 |
把 lint、type-check 分成不同 npm script,CI 中分別執行,讓失敗原因一目了然 |
| 依賴版本不一致導致型別錯誤 | 開發者本機使用較新/舊的 @types/*,CI 使用 lockfile 中的版本 |
在 CI 使用 npm ci,在本機同樣使用 npm ci 或 npm install 後 npm shrinkwrap,保持依賴一致 |
transpileOnly:true 造成未捕捉錯誤 |
只在開發時使用 transpileOnly,忘記在 CI 中執行完整檢查 |
永遠 在 CI 中執行完整 tsc,即使本地開發使用 transpileOnly 以提升速度 |
忽略 skipLibCheck 帶來的隱藏錯誤 |
大型專案中 skipLibCheck:true 會跳過第三方 .d.ts 檢查,可能隱藏相容性問題 |
僅在 CI 中開啟 skipLibCheck:false 進行全檢,或在本地開發階段偶爾手動跑 tsc --skipLibCheck false 以驗證 |
最佳實踐
- 分離型別檢查與編譯:在 CI 中使用
tsc --noEmit,在建置階段使用 bundler(Webpack/Vite)僅做轉譯。 - 保持 TypeScript 版本一致:在
package.json中明確指定typescript版本,並在 CI 中使用npm ci。 - 快取依賴與編譯結果:GitHub Actions 支援
actions/setup-node的cache: npm,能大幅縮短 CI 時間。 - 在 PR 評審時顯示型別錯誤:使用 GitHub Checks API 或 GitLab 的
code_quality報告,把tsc的輸出轉成可點擊的檔案/行號。 - 把型別錯誤視為阻斷條件:在 CI 設定
continue-on-error: false(預設),確保任何型別錯誤都會導致 pipeline 失敗。
實際應用場景
場景 1:大型單頁應用(SPA)
- 環境:React + Vite + Jest
- 需求:每次 PR 必須通過 lint、型別檢查、單元測試、E2E 測試才可部署至 staging。
- 解法:在 GitHub Actions 中設定四個獨立 job,使用
needs:讓type-check為第一關卡,失敗即阻止後續test、e2e、deploy。
場景 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 型別檢查,不只是提升程式碼品質,更是一種自動化的防護機制,能在每一次提交時即時阻止潛在錯誤流向生產環境。本文重點回顧如下:
- 設定嚴格的
tsconfig.json(strict:true、noEmit:true),讓編譯僅作為型別檢查。 - 把型別檢查抽成獨立 npm script,配合 lint、測試、打包形成完整的 CI pipeline。
- 在 CI 工作流(GitHub Actions / GitLab CI)中明確執行
tsc --noEmit,失敗即中斷後續步驟。 - 與建構工具(Webpack、Vite)結合 時,使用
transpileOnly或esbuild只做語法轉譯,避免重複型別檢查。 - 避免常見陷阱:依賴版本不一致、混合執行 lint+type-check、忘記在 CI 中關閉
noEmit。 - 最佳實踐:快取依賴、分離檢查與編譯、在 PR 中顯示型別錯誤、把型別錯誤視為阻斷條件。
透過上述步驟,你的專案將在 持續整合 階段就完成最嚴格的型別安全檢查,讓團隊更專注於功能開發,減少因型別錯誤導致的緊急修復與回滾。祝你在 TypeScript 與 CI/CD 的整合旅程中,寫出更安全、更穩定的程式碼!