網站搜尋

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 連結