# 型別不是安全感

> TypeScript 的型別在 runtime 不存在，tsc 綠燈只代表型別自洽。型別是幫你想清楚資料與邊界的工具，外部輸入要靠 schema validator 在邊界擋，別把型別當安全感。

原文：https://sheng.page/posts/typescript-types-are-not-safety/ · 發布：2025-04-12 · 作者：Sheng

我對 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 同時生出型別，內層程式碼直接拿驗證過的型別用：

```ts
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 翻新計劃，新漆未乾。