🎯 什麼情境該想到我
當你的型別變成一個「選配欄位袋(bag of optionals)」——一堆 ?: 欄位,而且沒人說得清哪些欄位會同時出現的時候。
⚙️ 怎麼用(步驟 / 公式)
病徵:太鬆的型別
type State = {
status: 'loading' | 'success' | 'error'
error?: string
data?: string
}問題有兩層:
- 存取時型別不準——判斷完
status === 'error',state.error依然是string | undefined。 - 能造出不可能發生的值——
{ status: 'loading', error: '...', data: '...' }完全合法。
解法:把欄位綁到它真正屬於的那個狀態
type State =
| { status: 'loading' }
| { status: 'error'; error: string }
| { status: 'success'; data: string }- 選一個判別欄位(discriminant):一個字面量型別、且每個成員都不同的共同屬性(這裡是
status)。 - 把每個狀態拆成獨立的物件型別。
- 只把該狀態真正需要的資料放進去。
現在判斷完 status === 'error',TS 的**收窄(narrowing)**會讓 state.error 直接是 string,不用再檢查 undefined。
成員多了可以各自抽成具名 type alias 再組成聯集,維持可讀性。
心法
讓不可能的狀態變成編不過的程式碼,而不是靠註解或紀律去記「loading 的時候不要看 error」。
🧪 我實際套用的紀錄
- 2026-08-28:(待填)
⚠️ 注意 / 什麼時候不適用
- 判別欄位必須是字面量型別(
'loading'這種),不能是string——否則 TS 無從分辨成員。 - 欄位真的到處都會出現時就不必拆。判別聯集是為了表達「互斥狀態」;本來就共用的欄位放共同部分即可。
- 成員太多會很囉唆。超過五、六個狀態時,先想想是不是狀態機該拆開,而不是繼續往聯集裡塞。
- Go 沒有這個東西(只有 interface 加型別斷言),從 Go 過來的直覺容易寫成一個大 struct 加一堆零值欄位——那正是「選配欄位袋」。
🔗 相關工具
- 工具-as-const與推論寬窄 —— 判別欄位常常因為推論太寬而失效(被推成
string而不是字面量),那張講怎麼修 - 工具-實體值物件與聚合 —— 同樣是「用型別表達領域規則」,那張是 DDD 的物件層版本
- 工具-封裝變化點 —— 互斥狀態集中在一個型別裡,之後加狀態只要改一處