🎯 什麼情境該想到我

當你的型別變成一個「選配欄位袋(bag of optionals)」——一堆 ?: 欄位,而且沒人說得清哪些欄位會同時出現的時候。

⚙️ 怎麼用(步驟 / 公式)

病徵:太鬆的型別

type State = {
  status: 'loading' | 'success' | 'error'
  error?: string
  data?: string
}

問題有兩層:

  1. 存取時型別不準——判斷完 status === 'error'state.error 依然是 string | undefined
  2. 能造出不可能發生的值——{ status: 'loading', error: '...', data: '...' } 完全合法。

解法:把欄位綁到它真正屬於的那個狀態

type State =
  | { status: 'loading' }
  | { status: 'error'; error: string }
  | { status: 'success'; data: string }
  1. 選一個判別欄位(discriminant):一個字面量型別、且每個成員都不同的共同屬性(這裡是 status)。
  2. 把每個狀態拆成獨立的物件型別
  3. 只把該狀態真正需要的資料放進去

現在判斷完 status === 'error',TS 的**收窄(narrowing)**會讓 state.error 直接是 string,不用再檢查 undefined。
成員多了可以各自抽成具名 type alias 再組成聯集,維持可讀性。

心法

讓不可能的狀態變成編不過的程式碼,而不是靠註解或紀律去記「loading 的時候不要看 error」。

🧪 我實際套用的紀錄

  • 2026-08-28:(待填)

⚠️ 注意 / 什麼時候不適用

  • 判別欄位必須是字面量型別'loading' 這種),不能是 string——否則 TS 無從分辨成員。
  • 欄位真的到處都會出現時就不必拆。判別聯集是為了表達「互斥狀態」;本來就共用的欄位放共同部分即可。
  • 成員太多會很囉唆。超過五、六個狀態時,先想想是不是狀態機該拆開,而不是繼續往聯集裡塞。
  • Go 沒有這個東西(只有 interface 加型別斷言),從 Go 過來的直覺容易寫成一個大 struct 加一堆零值欄位——那正是「選配欄位袋」。

🔗 相關工具