如何在 TypeScript 中處理 undefined 跟 null?
寫 TypeScript 時,對 undefined
跟 null
的使用時機常常混淆,很容易在程式運行時,遇到 undefined is not a function
或是 reading undefined property of X
之類的錯誤。這篇介紹了兩種解法,並說明優劣。
先來看 null
跟 undefined
的定義,可發現兩者都代表 value 缺失。
Undefined means a variable has been declared but has yet not been assigned a value. Null is an assignment value. It can be assigned to a variable as a representation of no value
最簡單的處理方式是用 if
判斷。這方式的缺點是,處理的情境並不是很精確,並不能真的表示一個預期的 value
存在。false
, null
, undefined
, 0
, empty string
or NaN
,在 if
block 中,都會被當成是 false
。 萬一我們預期的值是這些會被 falsify
的型別呢?
if (x) {
// yes x is there
} else {
// no x is no there
}
一個解法是用 Guard Function
。透過 isAbsent 這樣的輔助函式將 undefined
和 null
當成是同一件事情。這做法也滿危險的,undefined
和 null
並不是同一件事情。
// function which unifies null and undefined (name is example)
function isAbsent(x) {
return x === null || x === undefined
}
// for better readability lets create the opposite
function isPresent(x) {
return !isAbsent(x)
}
// now in action
if (isPresent(x)) {
// yes x is there
} else {
// no x is not there
}
當你有一個包含可選屬性的巢狀型別時,最常見的 undefined
錯誤發生在你試圖存取這個可選屬性(一個物件)的值,但沒有考慮到這個屬性可能不存在的情況。這時就會出現像是 undefined is not a function
或 reading undefined property of X
這樣的錯誤訊息。
來看一個例子。
const type User = {
name: string,
info?: {
age: number
}
}
function makeUser(name: string, info?: {age: number}) {
return {
name,
info
} as User
}
function printUserInfo(user: User) {
print(user.info.age);
}
makeUser`` 函數接受一個
name參數和一個可選的
info物件。它返回一個符合
User 類型的物件。
printUserInfo 函數接受一個 `User
物件作為參數,並顯示該用戶的 age
。但是,這裡沒有檢查 info
屬性是否存在,這可能導致問題。
const bob = makeUser('bob') // {name: "bob"}
const lisa = makeUser('lisa', {age: 14}) // {name: "lisa", info: {age: 14}}
printUserInfo(bob); // err
printUserInfo(lisa); // 14
這裡創建了兩個 User 物件:bob(沒有 info 屬性)和 lisa(有 info 屬性,其中 age 為 14)。當傳入 bob 時,會出現錯誤,因為 bob 沒有 info 屬性,所以 user.info.age 會嘗試訪問 undefined 的 age 屬性。當傳入 lisa 時,正常運作並打印出 14,因為 lisa 有 info 屬性,且其 age 為 14。
由於這種編成風格,很常使用,因此問題出錯時,如果 debug 沒開,Traceback 沒印出來,或是 logging 還沒寫,非常難找出來究竟是哪一個地方出錯。
有寫過 Haskell
的人,會自然地想用 Optional 的型別,來達到 Type Safety。嗯! 的確用這種方式,可以程式碼更為簡潔,並且強迫開發者好好地處理所有的例外分支。
type Optional<T> = Some<T> | Nothing
但 Maybe just Nullable? 這篇給出不同見解,作者 Pragmatic Maciej 覺得用 Optional 的方式,違反了 TypeScript 的內建設計,等於要再多學一套 DSL,不如直接用 Nullable 的方式就好。
下面是 Nullable 的定義,可以看出跟 Optional 很類似。
type Nullable<T> = T | (null | undefined)
type Optional<T> = Some<T> | Nothing
差異是 Optional 型別是一個 Functor
,你需要特殊的操作元去操作。在 [[fp-ts]] 中,你可以用 [[fp-ts Option]] 處理,而 Nullable
就不需要。不熟悉 Functional Programming 的朋友可以把 Functor 當成一個盒子,對裡面所有的操作,都必須透過 Functor
提供的介面操作。
const OptVal = Optinon.fromNullable(oooo);
console.log(OptVal); // Some(oooo)
const OptVal = Optinon.fromNullable(undefined);
console.log(OptVal); // None()
再來是 Nullable
在存取巢狀的屬性會比 Optional 簡單,可以直接用 imperative programing 風格撰寫。老實說,這對一般人來說比較不會讓腦袋打結。
posts
.find(post => post.id === id)
?.comments
.filter(comment => comment.active)
再來看 Optional 的版本,就要求你要思考 function 跟 type 的隱含的關係,各種值傳遞的路徑跟條件。有學過電路學的人,大概會比較容易接受這概念,在腦袋中模擬資料流向。
findInArr(posts, post => post.id === id)
.map(post => post.comments)
.map(comments => comments.filter(comment => comment.active))
作者主要質疑的是,採用 Optional 是否真的帶來足夠的好處。使用 Optional 意味著整個程式碼都必須遵循這種模式。然而,由於 TypeScript/JavaScript 的限制,Optional 不能在靜態分析階段捕捉到所有的錯誤。因此,作者對於採用 Optional 帶來的實際益處表示懷疑。
Both solutions, Nullable and Optional, in a static types land fix the Null issue. With TypeScript we know when value is optional. Because we know when to make if, or .map our code will not overuse nor conditions nor abstraction.
The TypeScript situation isn't different, I suggest to pick Optional only if we want to go fully into the functional rabbit hole, and you write mostly functions and expressions. You can consider two libraries to start - fp-ts and io-ts.
他認為你仍然可以用 Optional
,但因此放棄原生的語法,而去用另一套超語言,就得好好評出是否要投資這麼多時間去學習。
在比較 Nullable
和 Optional
這兩種風格時,它們都有各自的優勢。首先,使用 Nullable
和 Optional
都可以避免編寫額外的 isAbsent 檢查,這一點上它們各有千秋。然而,從學習成本的角度來看,Nullable
的門檻相對較低,而 Optional
則相對較高。
在不考慮數據驗證(validation)的情況下,我傾向於推薦使用 Nullable
。這是因為它簡單直接,適用於那些只需要判斷屬性是否存在的場景。但如果您的應用場景不僅僅是判斷可選屬性,還涉及到對數據進行驗證,那麼 Optional 可能是更合適的選擇。
驗證通常涉及檢查數據是否符合特定條件,但並不改變數據的表示形式。例如,您可能需要驗證一個字符串是否是有效的日期格式,但在驗證過程中,該字符串仍然被當作字符串處理。在這種情況下,Optional
提供了更多的靈活性和功能,可以更容易跟你使用的驗證函式庫整合起來。
我目前是用 Effect-ts 處理 API、表單驗證,所以是選 Optional
方式。個人經驗是這時間投資的很有價值。