模組二|選型、語言遷移與地基(Day 6–11)
標題不是我打錯。這是我在新專案的 TypeScript 設定檔裡看到的一行,一字不差:
{
"compilerOptions": {
"strict": false, // 啟用嚴格模式
}
}
註解說「啟用」,值是 false。
我第一次看到的時候笑了一下,然後意識到這行程式碼其實記錄了一整段歷史:有人在某個時間點把它從 true 改成 false,而且沒有改註解。
今天就從這一行開始,講我們怎麼導入 TypeScript,以及為什麼把最嚴格的檢查關掉了。
我原本以為這是「忘了開」。但往上追一層就發現不是。
專案的設定檔繼承自一份共用的基礎設定,而那份基礎設定裡寫著:
{
"strict": true,
"noImplicitOverride": true,
"noUnusedLocals": true,
}
框架的預設是嚴格的。是我們在專案層把它覆蓋成 false 的。
這代表有人做過一個決定:明知道基礎設定要求嚴格,還是把它關掉。所以這篇要回答的是「為什麼刻意關掉」,不是「為什麼忘了開」。
如果你沒用過 TypeScript:strict 是一個總開關,打開之後編譯器會啟用一整批嚴格檢查。最常見的三個:
function f(x),編譯器會說「x 是什麼型別?」user.name 在 user 可能是 null 的時候會報錯這些檢查全都是對的。長期來說一定要有。
但「對的」和「現在就該做」是兩件事。
你手上有一份 200 頁的報告,任務是把它從英文翻成中文。
這時候你請了一個超級嚴格的校對來幫你。他很厲害,連標點符號全形半形、括號前後空格都會挑出來。
於是你翻到第 3 頁的時候,桌上已經堆了 4,000 條標點符號的修改建議。
問題來了:在這 4,000 條裡面,哪幾條是「你翻錯意思」?
你找不到。因為它們混在一起了。
這就是我們的處境。遷移期要同時處理兩件事:
如果一開始就打開 strict,你會在畫面壞掉的時候分不清楚:是搬邏輯搬錯了,還是型別沒補完?
而 Day 6 講過同一個原則:在最不穩定的階段,不要增加額外的變因。
關掉 strict 不等於放棄 TypeScript。我去數了新專案的檔案型別分布:
| 副檔名 | 數量 |
|---|---|
.ts |
401 |
.vue |
396 |
.tsx |
32 |
.js |
8 |
JavaScript 檔案只剩 8 個。 整個專案實質上已經全面 TypeScript 化了。
我們做的是這三件事,它們的共同點是不需要 strict 就能拿到好處:
一、所有檔案都是 TypeScript。 .vue 檔一律用 <script setup lang="ts">。這一步本身就會逼出很多型別,因為編譯器至少會推導。
二、API 回傳有型別定義。 這是投報率最高的一件事,後端回什麼、前端拿到什麼,是所有 bug 的高發區(Day 23 會用數字證明)。
三、API 路徑集中管理。 專案有個慣例:每個 API 檔案先用列舉宣告所有路徑,再包成薄薄的函式:
enum Api {
GetOrderList = '/order/list', // 訂單列表
GetOrderDetail = '/order/detail', // 訂單詳情
ExportOrder = '/order/export', // 匯出
}
export function getOrderList(params: OrderListParams) {
return defHttp.get<OrderListResult>({ url: Api.GetOrderList, params })
}
好處很實際:路徑字串只出現一次。之後後端改路徑、或你要盤點「哪些 API 沒人用了」(Day 28 會講),都有一個唯一的地方可以查。
這三件事就吃掉了 TypeScript 大約八成的價值,而它們都不需要 strict。
因為對日常開發來說,TypeScript 最有感的不是「型別體操」,是IDE 會自動完成、按一下就跳到定義、改名字的時候不會漏。這些東西在 strict: false 底下一樣有。
any
講完好處,講代價。我去數了 any 的用量:
grep -rho ': any\|as any\|<any>\|any\[\]' src --include='*.ts' --include='*.vue' | wc -l
431 次,分布在 128 個檔案。
any 是 TypeScript 的逃生門:寫上去等於跟編譯器說「這個你別管」。用太多,型別系統就形同虛設。
431 次是多是少?以一個 800 多個檔案的專案來說,我覺得不算失控,但也絕對不是乾淨。它代表這個專案裡有一批地方,型別檢查是關著的。
而更關鍵的是:這些 any 是關掉 strict 的直接後果。如果 strict 開著,很多地方你不寫型別根本編不過,就會被迫想清楚;關掉之後,「先 any 一下」變成阻力最小的路徑。
有沒有很耳熟?這正是 Day 4 講的同一件事:沒有摩擦的捷徑,一定會被累積。

不過我還數了另一個東西:
grep -rho '@ts-ignore\|@ts-nocheck' src --include='*.ts' --include='*.vue' | wc -l
# → 9
只有 9 個。
這兩者的差別很重要:
any 是「這個值我不宣告型別」:範圍限縮在那個變數@ts-ignore 是「下一行的所有錯誤全部忽略」:不管是什麼錯後者粗暴得多。而全專案只有 9 個,代表大家在遇到型別報錯的時候,多半是去把它處理掉,而不是直接把檢查關掉。
所以我對這個專案的型別健康度的判斷是:寬鬆,但沒有腐爛。 這兩件事不一樣。
如果哪天 @ts-ignore 開始成長,那才是真的要警戒,因為那代表大家已經放棄跟編譯器溝通了。
這是我覺得最實用的一段。我們的想法是:不要問「什麼時候全部打開」,要問「哪一塊可以先打開」。
TypeScript 允許你分區域設定。所以合理的順序是:
反過來做(一次全開,然後花兩週修四千個錯)的問題不是工作量,是你會在同一個 PR 裡混進大量「為了讓它編過」的改動,而那種 PR 沒有人有辦法認真 review。
至於我們有沒有做到?老實說,目前還沒有。 這件事在待辦清單上,但一直沒有排進去,因為它屬於那種「不做也不會怎樣,做了也沒有人看得見」的工作。
這本身就是一課:漸進式導入最大的風險,不是導不完,是「暫時這樣」變成永久。 Day 6 講過同一句話,這裡再次應驗。
最後補一個當時沒想到、後來才理解的關聯。
Day 7 講過 Options API 的問題:this 是一個集散地,什麼都掛在上面。
而那種結構天生難標型別:this.someThing 從哪來?是 data、是 mixin 注入的、還是外掛掛上去的?編譯器也不知道,所以它推導不出型別。
Composition API 剛好相反:
const { initFormBase } = useFormBase()
這一行的型別是明確的,因為依賴被寫出來了。
換句話說:Composition API 讓依賴顯性化,而顯性的依賴才標得動型別。 這兩件事是互相加成的,所以一起做比分開做划算。
如果你正在考慮「先升 Vue 3 還是先導 TypeScript」,我的答案是:一起做,但型別的嚴格度先放寬。
一、any 會累積,而且不會自己消失。 431 個裡面,我猜大部分永遠不會被回頭處理。要有心理準備:你是在用未來的整理成本,換現在的遷移速度。
二、「之後再開 strict」很可能不會發生。 我們就是活生生的例子。如果你真的想做,要在計畫裡給它一個明確的時間點和負責人,否則它只是一句好聽話。
三、註解會說謊。 那行 "strict": false, // 啟用嚴格模式 就是證據。設定改了、註解沒改,而下一個看到的人會困惑三分鐘。這種小事累積起來,就是「這個專案的文件不能信」的開始。
strict 不等於放棄 TypeScript。 檢查 .js 檔案還剩幾個,比檢查 strict 是不是 true 更能反映真實的導入程度。any 的數量,也要看 @ts-ignore 的數量。 前者是寬鬆,後者是放棄,後者開始成長才是真的警訊。明天 Day 11 是模組二的最後一天,講建置:舊系統的建置指令為什麼要手動指定 6GB 記憶體、那些沒人敢刪的 webpack 設定,以及換成 Vite 之後真正改變的東西,不是 build 時間,是別的。