TypeScriptStrict ModeFrontend EngineeringCode ReviewCI

TypeScript strict 遷移指南:前端團隊導入策略、Code Review 與 CI 實務

整理 TypeScript strict migration 的分階段導入策略,聚焦 strict family 爆點、code review 規則與 CI gate,幫團隊穩定把風險前移到 PR。

· 5 分鐘閱讀

這篇會帶你把 strict 從「設定檔選項」變成可執行的團隊遷移流程,並且用 code review 與 CI 防止回退。


前言

最近很多團隊在談 TypeScript strict,不是因為突然流行,而是因為時程壓力真的到了。TypeScript 6.0 RC(2026-03-06 公告)開始把 strict 預設改為 true,6.0 正式版也已在 2026-03-23 發布。這代表新專案「預設就更嚴格」,但不代表所有舊專案會在同一天自動爆炸。

要先釐清三種情境:

  1. RC / 正式版新建專案:如果沒明確覆蓋設定,strict 預設會是 true
  2. 已存在且明確寫 "strict": false 的舊專案:行為不會瞬間改變,但技術債會持續累積。
  3. 既有專案沒寫 strict、只靠舊版預設:升級 compiler 時會面臨行為差異,需要先做風險盤點。

為什麼現在就要處理 strict

strict 的價值不只是「型別更完整」,而是把 production 事故往前移到 PR 階段。對前端團隊來說,這通常直接改善兩件事:

  1. 重構風險可視化:過去 runtime 才炸的 null/undefined 問題,會在 type-check 就被攔下。
  2. Review 成本下降:reviewer 不必猜函式參數是否安全,編譯器先做第一層 gate。

如果你把 strict 當作「一次性清債」,專案很容易卡住;把它當作持續降低錯誤注入率的機制,才比較接近真實團隊運作。


strict 到底包含什麼

strict: true 不是單一檢查,而是一組 strict family 旗標。以 TypeScript 6.0 文件來看,常見包含以下項目:

Flag你會感受到的改變
noImplicitAny不再容許隱含 any 混進資料流
noImplicitThisthis 來源不明時會被攔截
strictNullChecksnull/undefined 需要顯式處理
strictFunctionTypes回呼參數型別不相容時會報錯
strictBindCallApplycall/apply 參數檢查變嚴格
strictPropertyInitializationclass 欄位初始化更明確
useUnknownInCatchVariablescatch (err) 預設視為 unknown
strictBuiltinIteratorReturniterator return 型別推論更嚴謹
alwaysStrictemit "use strict" 並以 strict mode 解析

以上是目前最常用到的 strict family,實際行為仍以當前 TypeScript 版本文件為準。


遷移順序:先收斂錯誤面,再提高門檻

實務上不建議全專案直接翻 "strict": true。比較穩的路線是分階段降低爆炸半徑。

Phase 0: 盤點現況與邊界

{
  "compilerOptions": {
    "strict": false,
    "noEmit": true
  }
}

先確認 tsc --noEmit 能穩定跑完,並且把目前錯誤分類(nullish、implicit any、函式參數不相容、第三方型別不完整)。這一步的目標不是修完,而是建立 backlog。

Phase 1: 先開 strictNullChecks + noImplicitAny

{
  "compilerOptions": {
    "strict": false,
    "strictNullChecks": true,
    "noImplicitAny": true
  }
}

這兩個 flag 通常最影響品質,也最容易在 code review 形成共識。等錯誤數量降到可控後,再往下一階段走。

Phase 2: 打開函式與呼叫安全

{
  "compilerOptions": {
    "strict": false,
    "strictNullChecks": true,
    "noImplicitAny": true,
    "strictFunctionTypes": true,
    "strictBindCallApply": true
  }
}

這階段會讓高階函式、SDK wrapper、utility 層出現最多改動,建議搭配 pair review。

Phase 3: 收斂到 strict: true

{
  "compilerOptions": {
    "strict": true
  }
}

最後才改成單一 strict: true,避免長期維護一串手動旗標造成認知負擔。


常見爆點 1: strictFunctionTypes 不是形式檢查,而是可避免 runtime crash

下面是官方行為可以重現的例子。

function acceptStringOnly(x: string) {
  console.log(x.toLowerCase());
}

type StringOrNumberHandler = (value: string | number) => void;

// strictFunctionTypes 開啟後,這行會報錯
// 因為呼叫端可能傳 number,但 acceptStringOnly 只吃 string
const handler: StringOrNumberHandler = acceptStringOnly;

handler(42); // 如果放行,runtime 會在 toLowerCase 爆掉

這不是「型別系統太嚴」,而是把本來就不安全的 assignability 擋下來。尤其在 callback-heavy 程式碼(event handlers、表單驗證、資料轉換 pipeline)很重要。


常見爆點 2: strictBindCallApplycall/apply 回到可審查狀態

很多 codebase 會在 util 層大量用 call/apply,如果沒型別檢查,參數數量或 union literal 很容易悄悄失真。

function formatPrice(currency: "TWD" | "USD", amount: number) {
  return `${currency} ${amount.toFixed(2)}`;
}

formatPrice.call(undefined, "TWD", 99); // ✅
formatPrice.call(undefined, "JPY", 99); // ❌ "JPY" 不在 union

formatPrice.apply(undefined, ["USD", 120]); // ✅
formatPrice.apply(undefined, ["USD"]); // ❌ 少一個 amount

遷移時可以先鎖定「共用 helper + SDK adapter + API client」三類模組,通常能最快看到收益。


常見爆點 3: value == null?? 不是互斥,而是語義不同

過去很多文章把這題寫成「一律用 ===」,實務上不精準。你要先決定語義。

function normalizeName(input: string | null | undefined) {
  if (input == null) {
    // 這裡是刻意同時攔 null 與 undefined
    return "Anonymous";
  }
  return input.trim();
}

function displayQuota(quota: number | null | undefined) {
  // 只在 nullish 時給預設值,不影響 0
  return quota ?? 100;
}

規則很簡單:

  1. 你要做 nullish guard(同時處理 null | undefined)時,value == null 可以是可讀又精準的寫法。
  2. 你要做 fallback expression 時,優先用 ??,避免誤傷 0""false

把 strict 寫進 Code Review 與 CI

只改 tsconfig 不夠,流程才是關鍵。下面是一個前端團隊可直接落地的最小模板。

PR Checklist

  1. 新增或修改的 public function 不可引入隱含 any
  2. @ts-ignore 必須附 issue 連結與移除期限。
  3. as unknown as 視為例外寫法,需在 PR 描述交代原因。
  4. 若動到 call/apply,需貼出對應型別測試或編譯錯誤截圖。

CI Gate

pnpm tsc --noEmit
pnpm eslint .
pnpm test

如果是 monorepo,建議先做增量 gate(只擋新變更),再逐步擴大到全 repo。


常見問題 / 注意事項

1. 我們是舊專案,能不能永遠維持 strict: false

可以,但每次升級 TypeScript、框架或第三方型別時,整體維護成本通常會更高,尤其在多人協作下,錯誤會更晚被發現。

2. strict 一開就 800 個錯誤,怎麼辦?

先分桶,不要硬吃:

  1. nullish 類(高風險,先修)
  2. implicit any 類(高頻,建立範本後批次修)
  3. 第三方型別缺口(可先寫 local declaration 或 wrapper)

總結

strict migration 的核心不是「把錯誤修到 0」,而是建立一條不會回退的工程路徑:先分階段降低風險,再用 review 規則與 CI 保持成果。 下一步建議是先建立 strict 錯誤分類清單(至少分成 nullish / any / function assignment 三類),再把 strictNullChecksnoImplicitAny 納入正式 gate。