TypeScript 撰寫與型別驅動設計的實作經驗分享
經驗分享
為什麼走向「型別先想」而不是「先寫再補」
在業務規則多、表單與流程分支也多的前端專案裡,若只靠執行時再靠any或是鬆散的Object結構,很容易出現「某個列舉值忘記處理」、「同一個概念在不同檔案各寫一套字串」、「某個欄位沒有處理」這類問題。把領域裡的狀態、條件種類、導頁規則先收斂成清楚的型別模型,等於多了一層在編譯期就會叫錯的設計文件;之後改需求時,常常是改一處型別定義,編譯器就幫你列出所有必須跟著動的地方。
用「可區分的聯集」描述業務,而不是一大包 optional
當同一塊資料其實是「好幾種長相完全不同的東西」時,與其用一堆可選欄位疊在同一個型別上,不如用共用的辨識欄位(例如種類標籤)把變體分開。這樣在實作上可以做到:依種類縮窄之後,只能存取該種類該有的欄位,避免在執行時期才發現某個組合根本不合法。維護心得是:聯集寫得越貼近產品語言,之後看程式的人越不用猜「這個欄位到底哪種情況才有意義」。
讓「對照表」本身成為單一事實來源
像「每一種設定類型對應到哪一種來源資料形狀」「每一種條件類型要怎麼建立預設值」這類規則,若散落在各元件裡用字串比對拼湊,之後加一種新類型很容易漏改某一層。實務上我會傾向把對照關係集中成結構化的對照表(物件或映射),並用型別約束去要求:表上必須涵蓋所有已定義的 key,缺一不可。這樣新增一個業務種類時,編譯錯誤會直接指出「你還沒在對照表補上這一列」,而不是等上線後才在某些特殊的情況下產生不可預期的錯誤。
泛型與索引型別:把「同一套規則」用在很多種輸入上
當函式的回傳形狀必須跟著輸入的「標籤」一起變(例如依條件種類回傳對應的條件物件、依設定種類決定路由與參數)時,若只用寬鬆的聯集當回傳型別,呼叫端還是得自己斷言或再判斷。個人認為盡量讓泛型參數把「輸入上的那個標籤」一路傳下去,讓回傳與輸入在型別上互相關聯;這樣在提示的時候可以自然得到較窄、較正確的型別,減少重複的防禦性判斷。
取捨與成本
型別驅動設計的前置思考與型別檔維護都有成本,開發者甚至是團隊要對「哪些領域值得寫的複雜」有共識。通常最常會撰寫的部分一定是與後端溝通的API相關規範與型別定義。其他部分則是依據專案的性質與需求來決定,並且透過開發者自身的專業能力去判斷需要的是複雜或是簡單的定義方式。
編譯通過不代表產品行為就完整
型別多半只保證程式靜態上自洽,無法保證執行時資料永遠合約。API、快取或版本不一致仍可能讓畫面空白或卡死。因此除了盡量把分支與對照表寫齊,仍要為錯誤與未知情況預留一致的 fallback 與 UI,而不是假設編譯通過就等於體驗沒漏洞。
小結
截至目前為止的經驗來說,TypeScript 對我而言比較像「把規則寫進型別裡的利器」。用聯集表達業務邏輯、用對照表與 exhaustive 約束降低漏改、用泛型保留型別資訊。程式碼範例會隨專案演進而換,雖然加上型別會讓寫法稍微繁瑣,但是可以大幅提升程式碼的可讀性與可維護性。在日後如果需要擴展或是重構,可以大幅降低維護成本。
