型別不是安全感

我對 TypeScript 沒有意見,有意見的是把型別當安全感的用法。型別系統原本是幫你想清楚資料長什麼樣、邊界在哪裡的工具,結果在不少團隊裡,它變成一種儀式:型別越多越複雜,心裡越踏實,好像 tsc 一過,這段程式就「安全」了。這是錯覺,而且是會拖慢迭代的那種錯覺。

型別是用來思考的,不是用來壯膽的

型別最有價值的時候,是逼你先回答幾個問題:這個函式吃進來的資料結構是什麼,哪些欄位真的會存在,哪些是 optional,回傳的東西呼叫端怎麼用。你把這些寫成型別,等於把腦袋裡模糊的假設列出來檢查一遍,這個過程本身就比型別檔本身值錢。

問題出在另一個方向。很多 codebase 你打開一個檔案,迎面而來是一整片 generic 體操,conditional type 套 mapped type、再 infer 出第三層,光看懂型別在做什麼就要花十分鐘。然後你發現,這坨型別其實只是在描述「一個有三個欄位的物件」。這不是在思考資料,這是在用型別系統證明自己很會用型別系統。

我自己的判準很簡單:如果一個型別需要寫註解才看得懂,它大概已經在包裝一個沒想清楚的設計。真正想清楚的資料模型,型別會跟著變簡單,因為資料本身就單純。當你發現得靠條件型別去勉強拼出一個介面,通常代表上游的資料結構該重切,而不是型別該變聰明。型別變複雜,往往是設計沒整合好的徵兆,不是工程能力的展示。

過度型別化還有一個隱性成本:迭代變慢。需求一改,那坨精雕細琢的 generic 跟著崩,你得花時間哄編譯器重新開心,而不是花時間改業務邏輯。型別應該服務你改程式的速度,不是反過來綁架你。

邊界才是重點:tsc 過了不代表資料對

這是最容易被忽略的一點:TypeScript 的型別在 runtime 不存在。編譯完就被抹掉,執行期沒有任何東西在幫你檢查。你宣告 function f(u: User),不代表跑起來進到 f 的真的是 User

只要資料是從外面進來的,API response、使用者表單、localStorage、第三方 webhook、訊息佇列,型別就保證不了任何事。後端多塞一個 null、欄位改名、某個 optional 突然變必填,你的 User 型別完全不會抗議,tsc 一樣綠燈,然後 production 在某個你最意想不到的地方炸開。

真正擋得住問題的是邊界上的 runtime 驗證。資料進系統的那一刻就用 schema validator 檢一遍,像 Zod 或 Valibot 這種,從 schema 同時生出型別,內層程式碼直接拿驗證過的型別用:

import { z } from "zod";

const User = z.object({
  id: z.string().uuid(),
  name: z.string().min(1),
  email: z.string().email().optional(),
});

type User = z.infer<typeof User>;       // 型別從 schema 來
const user = User.parse(await res.json()); // runtime 真的擋

重點不在於用哪一套,而在於心態:把驗證放在邊界,內層才能信任資料。type annotation 給的是編譯期的自洽,runtime 的 parse 給的才是執行期的保證。前者讓你寫程式時方便,後者讓系統在收到髒資料時不會默默壞掉。兩件事不能互相取代,而大部分把型別當安全感的人,剛好把後者整個漏掉了。

「能 build」不等於「設計合理」

tsc 沒報錯,只說明一件事:你的型別自己跟自己對得起來。它不會告訴你邏輯對不對、邊界守住沒、資料流合不合理。型別自洽是很低的標準,低到一段邏輯完全錯誤的程式也能輕鬆通過。

我看過太多 PR 的潛台詞是「型別都過了所以應該沒問題」。但編譯器只驗證它管得到的那一小塊,它管不到的東西,包括幾乎所有真正會出事的地方,runtime 的資料、外部系統的行為、業務規則的正確性,它一個字都不會幫你說。把「能 build」當成品質保證,是把工具的能力範圍想得太大了。

務實的做法其實很無聊:strict mode 該開,any 該避,unknown 配型別收窄是好習慣,這些都對。但別把型別當主角。它是幫你思考資料的工具,是邊界驗證的搭檔,不是一個你可以躲在後面、過了就安心的盾牌。型別系統是工具,當你開始為了型別好看而扭曲設計,方向就已經反了。

重點整理

  • 型別的價值在逼你想清楚資料特性與邊界,不在讓你心裡覺得安全。
  • 型別變複雜通常是設計沒整合好的徵兆,不是工程能力的展示。
  • TypeScript 型別在 runtime 不存在,外部輸入只能靠 schema validator 在邊界擋。
  • 「tsc 過了」只代表型別自洽,跟邏輯對不對、資料髒不髒無關。
  • strict mode 開、any 避,型別是工具,別讓它主導設計。

觀點 Sheng,內文 Claude 協助 · 列入 20260613 blog 翻新計劃,新漆未乾。