TypeScript 中一般函數與函數表達式之間不同類型縮窄行為
function foo(item: string | null) {
if (!item) return null
return () => {
function regular() {
return item
}
const arrow = () => item
console.log(regular().toLowerCase()) // Error: Object is possibly 'null'.
console.log(arrow().toLowerCase()) // Pass
}
}
遊樂場在這裡
在函數 foo 中,我寫了兩個巢狀函數。一個是一般函數,另一個是箭頭函數。雖然我預期兩個返回值的類型都應該被縮小為 string,但事實是 regular() 的類型為 string | null。
起初我以為可能是因為函數參數 item 是可變的。(在此問題中提到過) 所以我手動將它轉換為 const,但問題仍然存在:
function foo(item: string | null) {
const itemCopy = item // Make sure TypeScript know it is immutable
if (!itemCopy) return null
return () => {
function regular() {
return itemCopy
}
const arrow = () => itemCopy
console.log(regular().toLowerCase()) // Error: Object is possibly 'null'.
console.log(arrow().toLowerCase()) // Pass
}
}
遊樂場在這裡
還提到這似乎與箭頭函數無關。將箭頭函數改為普通函數表達式也會顯示相同的行為。
function foo(item: string | null) {
if (!item) return null
return () => {
function regular() {
return item
}
const exp = function () { // Now the only difference is one is function expression while the other is not.
return item
}
console.log(regular().toLowerCase()) // Error: Object is possibly 'null'.
console.log(exp().toLowerCase()) // Pass
}
}
遊樂場在這裡
那為什麼會這樣呢?這種行為是設計上刻意如此嗎?
答案#1
根據 microsoft/TypeScript#32300,這似乎按預期運作。從根本上說,函數 宣告/語句 與函數 表達式 的行為不同,因為函數宣告會被提升。被提升的函數宣告可以在程式碼中定義出現之前被呼叫,而非提升的函數表達式則不行:
function foo(item: string | null) {
if (!item) return null
return () => {
regular(); // okay
exp(); // error
function regular() {
return item
}
const exp = function () {
return item
}
}
}
這意味著對控制流程分析有不同的影響。看起來對於提升的函數,TypeScript 只是放棄,悲觀地假設任何被閉包的變量都沒有被縮小,而對於函數表達式,它樂觀地假設早期的任何縮小仍然存在。目前尚不清楚這些行為哪一個是“正確的”,但它確實按照設計運作。
在 microsoft/TypeScript#9998 中,對於在封閉變量面前收窄的一般問題有深入討論。TypeScript 做了許多簡化假設,這些假設在許多情況下工作得很好,但在邊緣情況下會出現不幸的偽陰性和偽陽性。通常當人們發現不一致時,結果並不是一個錯誤,而是一個帶有缺點的設計選擇。
程式碼的 Playground 連結
