一個把 amount 從 number 改成 string 的後端,讓我徹底改用 TypeScript
那個顯示成 NaN 的金額
我是寫 Go 出身的後端,會認真用 TypeScript,是因為一次很蠢但很痛的線上 bug。
那時候我兼著做一個 Vue 的後台。某天後端同事(就是我自己另一個 repo)把一個 API 的 amount 欄位,從數字改成了字串——因為前面講過的理由,金額要用字串傳才不會掉精度。改得很對。問題是前端這邊完全不知道,程式碼裡還是把 amount 當數字在做加總、做格式化。上線之後,後台的金額欄位全部顯示成 NaN。
沒有人報錯,編譯也過,就是畫面上一排 NaN。客戶截圖來問的時候我才發現。追下去,根因就是一句話:前後端對這個欄位的型別,認知不一致,而沒有任何機制在我改壞的當下攔住我。
那天之後我對 TypeScript 的態度就變了。它對我最大的價值,從來不是那些花俏的型別體操,而是這個。
它最大的價值:前後端的契約
TypeScript 真正幫到我的,是把「這個欄位到底是什麼型別」這件事,從口頭約定、從腦袋裡的記憶,變成程式碼裡編譯器會檢查的契約。
我現在的習慣是,API 的回傳型別在前端定義成明確的 interface。後端欄位一改,前端用到的地方型別對不上,編譯就紅給你看——在你上線之前,不是在客戶截圖之後。那次 NaN 事件如果 amount 的型別有定義好,我把它從 number 改成 string 的瞬間,所有還把它當數字用的地方都會編譯失敗,我根本不可能把那個 bug 帶上線。
我真正常用的型別功能
用了幾年,我發現自己天天在用的其實就那幾個,不是什麼高深的東西:
- 聯合型別(union):一個訂單狀態只會是那幾種字串,我就用聯合型別把它們列出來,打錯字編譯就報錯,不用等到執行期。
- 工具型別(Partial、Pick、Omit 這些):從一個大的 interface 衍生出「更新用的部分欄位版本」「只挑幾個欄位的版本」,不用重寫一份,改一個地方全部同步。
- 泛型:主要用在包裝 API 回應那種「外層結構固定、內層資料型別會變」的場景,寫一次到處套。
至於那些很炫的條件型別、映射型別,我偶爾會用,但說實話日常八成的價值,就來自上面這三個樸素的功能。
寧可嚴格,也不要 any 到處跑
我的 tsconfig 一定開 strict。剛開始會覺得很煩,這也要標型別、那也不給你 null,但這些「煩」,本質上都是編譯器在幫你把執行期才會爆的錯,提前到編譯期。那個 NaN 就是典型的「執行期才爆」,而它本來完全可以在編譯期被擋下來。
any 是逃生門,不是常態。 偶爾真的搞不定某個第三方型別,用一下 any 讓自己先過,可以。但如果你的程式碼裡 any 滿天飛,那你等於把 TypeScript 的保護傘收起來,回到那個「改壞了也沒人告訴你」的世界——那你用它幹嘛?
小結
我對 TypeScript 沒有什麼信仰,我在乎的很實際:它能不能在我改壞東西的那一刻攔住我。那次一整排的 NaN,就是「沒有契約」的代價。所以如果你問我 TypeScript 的進階型別哪個最該學,我的答案很無趣——先把 API 的型別老老實實定義好、strict 開起來、少用 any。這些不炫,但這些是真的會在某個上線前的下午救你一命的東西。
留言討論
有想法、有不同經驗、或想糾正我?歡迎在下面留言,免註冊,填個暱稱就能留。