網站搜尋

TypeScript:聯合類型的對象可能為 null


我有一個基礎介面,其中至少需要兩個屬性中的一個。

interface Base {
    someProp: { subProp: string; } | null;
    condProp1?: boolean;
    condProp2?: boolean;
}

interface WithProp1 extends Base {
    condProp1: true;
}

interface WithProp2 extends Base {
    condProp2: true;
}

export type MyType = WithProp1 | WithProp2;

當我在檢查過 someProp 不為 null 並且在檢查 condProp1 或 condProp2 是否為真之後嘗試使用 MyType,編譯器仍然認為 someProp 可能是 null。

function doSomething(myType: MyType) {
    if (myType.someProp && (
        myType.condProp1 && myType.someProp.subProp === 'foo' ||
        myType.condProp2 && myType.someProp.subProp === 'bar')) {
        console.log("It's true, isn't it?  It's really true.");
    }
}

在 doSomething() 中,myType.someProp.subProp 出現編譯時錯誤,提示 someProp 可能為 null。如果我在函數簽名中將 MyType 換成 Base,則不會發生這種情況。

// Compiles fine
function doSomethingBase(myType: Base) {
    if (myType.someProp && (
        myType.condProp1 && myType.someProp.subProp === 'foo' ||
        myType.condProp2 && myType.someProp.subProp === 'bar')) {
        console.log("It's true, isn't it?  It's really true.");
    }
}

將順序改成 myType.someProp.subProp === 'foo' && myType.condProp1 能作為第一行的變通方法,但對第二行無效。有人知道為什麼會這樣或怎麼解決嗎(我可以使用 !,但其實不應該需要)?看起來像是編譯器的錯誤。

我正在使用 TSC 版本 3.5.3。TypeScript 遊樂場

答案#1

我懷疑這實際上是編譯器錯誤;我對內部原理並不是非常熟悉,所以不能確定,但我懷疑編譯器把初始的保護語句評估為類型 Base,然後把內部的語句評估為 WithPropX,並且遺失了 myType 作為 WithPropX 已經被防護過 null 的 someProp 的信息。

你可以透過反轉檢查的順序來解決這個問題,這樣更具體的類型推斷會在條件中較早發生(然後 someProp 的 null 檢查會在編譯器知道 myType 是 WithPropX 而不是 Base 的時候發生):

function doSomething(myType: MyType) {
    if (myType.condProp1 && myType.someProp && myType.someProp.subProp === 'foo' ||
        myType.condProp2 && myType.someProp && myType.someProp.subProp === 'bar') {
        console.log("It's true, isn't it?  It's really true.");
    }
}

這個問題似乎可能與你在這裡觀察到的情況有關。

答案#2

我喜歡 @Chris Helad 說的:

我懷疑編譯器將初始的保護子句評估為 Base 類型,然後將內部子句評估為 WithPropX,並且失去了 myType 作為 WithPropX 已經對 null 的 someProp 進行過保護的信息。

所以,編譯器,可能,是這樣工作的:

function doSomething(myType: MyType) {
  if (myType.someProp && ( // myType is type of WithProp1 | WithProp2 AND someProp is defined
    myType.condProp1 && myType.someProp.subProp === 'foo' || // condProp1 is defined, so, myType is typeof WithProp1 (forgot that someProp is defined)
    myType.condProp2 && myType.someProp.subProp === 'bar')) { // condProp2 is defined, so, myType is typeof WithProp2 (forgot that someProp is defined)
    console.log(`It's true, isn't it?  It's really true.`);
  }
}

為了擊敗它,你可以定義一個簡單的類型保護來告訴編譯器實際的類型:

function hasProp(t: MyType): t is MyType & {someProp: NonNullable<MyType['someProp']>} {
  return !!t.someProp;
}

function doSomething(myType: MyType) {
  if (hasProp(myType) && (
    myType.condProp1 && myType.someProp.subProp === 'foo' ||
    myType.condProp2 && myType.someProp.subProp === 'bar')) {
    console.log(`It's true, isn't it?  It's really true.`);
  }
}