TypeScript - 縮小聯合型別的條件型別
對不起這個標題有點模糊,但我的問題來自於我並不完全理解 TS 在這種情況下的行為,所以不確定該怎麼稱呼它。基本上,問題是——為什麼這段代碼會失敗:
type Broken<T extends string | number> = T extends string ? T : null
let foo:Broken<string>
let value: string
foo = value
let value2: string | number
// this error makes sense to me: value2 could be a number
foo = value2
if(typeof value2 === "string")
{
// and it makes sense to me this works, because now we know they are both strings
foo = value2
}
function testBroken<T extends string | number>(value: T ){
let bar: Broken<T>
// i guess i expect this error, but i'm not sure i understand why TS says the error
// is that "string" is not assignable to Broken<T>
bar = value
if(typeof value === "string"){
// Why is this an error? if "bar" is narrowed to string don't we know that Broken<T>
// is also "string"?
bar = value
}
}
這裡是遊樂場
更具體地說,為什麼對 bar 的賦值會產生錯誤(尤其是第二個賦值),但對 foo 的第二個賦值卻不會。我認為這與條件類型中的類型分佈有關,但我真的無法理解其中的原理。
有人可以解釋這裡發生了什麼嗎?(注意:這個問題更多是關於理解 TS 在這裡的操作,而不是想出類型的解決方法)。
答案#1
這目前是 TypeScript 的一個設計限制。編譯器會延遲評估依賴尚未指定的泛型類型參數的條件類型,因此它無法真正驗證任何東西是否可以分配給這些類型。在你的情況下,Broken<T> 是一個條件類型,而在 testBroken() 的實現中,它所依賴的泛型類型參數 T 尚未被指定(它只有在你調用testBroken() 時才會被指定)。因此對編譯器而言,Broken<T> 本質上是不可見的,它無法看到 value 是否可以分配給它,即使你能將 T 或 value 縮小到 string 的子類型。
GitHub 上圍繞這個問題的典型案例可能是 microsoft/TypeScript#33912。它建議編譯器應該使用控制流分析來縮小 value,從而可能將 T 縮小到 string 的子類型,然後使用這個來將 Broken<T> 縮小到 T。如果你想看到這個情況發生,你可能想去那個問題頁面並點個 👍。在此之前,只有一些變通的方法可用。
既然問題不是關於解決方法,我就不多做詳細說明,但簡單來說,你總是可以使用類型斷言來平息警告:
function testBrokenAssertion<T extends string | number>(value: T) {
let bar: Broken<T>
if (typeof value === "string") {
bar = value as Broken<T>
}
}
你甚至可以像 microsoft/TypeScript#33912 中所示的那樣,將這些斷言重構到一個輔助函數中:
function testBrokenHelper<T extends string | number>(value: T) {
let bar: Broken<T>
bar = conditionalProducingIf(value, isString, () => value, () => null) // okay
}
程式碼的 Playground 連結
