寫 TypeScript 時,對 undefinednull 的使用時機常常混淆,很容易在程式運行時,遇到 undefined is not a function 或是 reading undefined property of X 之類的錯誤。這篇介紹了兩種解法,並說明優劣。

先來看 nullundefined 的定義,可發現兩者都代表 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 當成是同一件事情。這做法也滿危險的,undefinednull 並不是同一件事情。

// 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 functionreading 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 ,但因此放棄原生的語法,而去用另一套超語言,就得好好評出是否要投資這麼多時間去學習。

在比較 NullableOptional 這兩種風格時,它們都有各自的優勢。首先,使用 NullableOptional 都可以避免編寫額外的 isAbsent 檢查,這一點上它們各有千秋。然而,從學習成本的角度來看,Nullable 的門檻相對較低,而 Optional 則相對較高。

在不考慮數據驗證(validation)的情況下,我傾向於推薦使用 Nullable。這是因為它簡單直接,適用於那些只需要判斷屬性是否存在的場景。但如果您的應用場景不僅僅是判斷可選屬性,還涉及到對數據進行驗證,那麼 Optional 可能是更合適的選擇。

驗證通常涉及檢查數據是否符合特定條件,但並不改變數據的表示形式。例如,您可能需要驗證一個字符串是否是有效的日期格式,但在驗證過程中,該字符串仍然被當作字符串處理。在這種情況下,Optional 提供了更多的靈活性和功能,可以更容易跟你使用的驗證函式庫整合起來。

我目前是用 Effect-ts 處理 API、表單驗證,所以是選 Optional 方式。個人經驗是這時間投資的很有價值。