網站搜尋

TypeScript:將聯合類型「縮窄」到其中一個成員


假設我有這個聯合類型:

type Foo = {tag: 'A', name: string} | {tag: 'B', value: number}

我正在嘗試編寫一個函數,該函數獲取一個標籤值('A' | 'B'),然後在一個 Foo 列表中進行一些特定業務的查找,並返回匹配的元素。就這個問題而言,我們可以將這個函數視為非常簡單的函數:

function findFirst(foos: Foo[], tag: 'A' | 'B') {
  for (const foo of foos) {
    if (foo.tag === tag) { 
      return foo 
    }
  }
  return undefined
}

假設我現在呼叫這個函數,並將 'A' 作為第二個參數傳入。很明顯,它要麼返回 undefined,要麼返回 {tag: 'A', name: string} 物件。在這種情況下,它根本不可能返回 {tag: 'B', value: number}。

不過,TSC 似乎無法推斷這一點。當寫出像以下的表達式時:

findFirst([{tag: 'A', name: 'X'}], 'A')?.name

TSC 出現錯誤 Property 'name' does not exist on type 'Foo'.。

我猜我需要透過明確定義 findFirst() 的回傳類型來幫助編譯器推斷正確的型別,但我不確定該怎麼做。

(遊樂場)

答案#1

你需要在函數中添加一個類型參數,以捕捉傳入的標籤的實際類型。然後,你需要使用 Extract 預定義條件類型來過濾 Foo 聯合類型:

function findFirst<T extends Foo['tag']>(foos: Foo[], tag: T) {
  for (const foo of foos) {
    if (foo.tag === tag) { 
      return foo as Extract<Foo, { tag: T }>
    }
  }
  return undefined
}

遊樂場連結

答案#2

你可以使用函數重載來聲明結果,而不必使用類型轉換。

type FooA = {tag: 'A', name: string};
type FooB = {tag: 'B', value: number};

type Foo = FooA | FooB 

function findFirst(foos: Foo[], tag: 'A'): FooA | undefined;
function findFirst(foos: Foo[], tag: 'B'): FooB | undefined;
function findFirst(foos: Foo[], tag: 'A' | 'B') {
  for (const foo of foos) {
    if (foo.tag === tag) { 
      return foo 
    }
  }
  return undefined
}


findFirst([{tag: 'A', name: 'X'}], 'A')?.name
findFirst([{tag: 'A', name: 'X'}], 'B')?.value

// TS Error as expected
findFirst([{tag: 'A', name: 'X'}], 'A')?.value


遊樂場連結