簡體   English   中英

打字稿遞歸函數中嵌套數組的正確類型

[英]Correct type for nested array in a recursive function in typescript

我正在嘗試在打字稿中實現一個與 lodash 的_.flattenDeep(array)非常相似的函數flattenDeep ,但是我遇到了一些類型定義問題,這是我的代碼:

export interface DeepArray<T> extends Array<T | DeepArray<T>> {}

export const flattenDeep = <T>(array: DeepArray<T>): T[] => {
  return [].concat(...array.map(arr => {
    if (Array.isArray(arr)) {
      return flattenDeep(arr)
    } else {
      return arr
    }
  }))
}

flattenDeep([1, [2, [3, [4]], 5]]) // should return [1, 2, 3, 4, 5]

到目前為止它看起來不錯,當我測試它時,功能本身沒問題,但是flattenDeep()函數推斷出錯誤的返回類型:

const arr = [1, [2, [3, [4]], 5]]
const flattenedArr = flattenDeep(arr)

// inferred type is:
// const flattenedArr: (number | (number | (number | number[])[])[])[]
// but it should expected to be number[] or Array<number>? since the final result is a one dimensional array

那么我如何解決這個問題以提供正確的類型?

問題在於,由於DeepArray<T>是遞歸類型的並且在結構上與(T | DeepArray<T>)[] ,因此沒有對應於DeepArray<T>的唯一T 例如,類型DeepArray<number>(number | DeepArray<number>)[]兼容,因此與(number | (number | DeepArray<number>)[])[](number | (number | DeepArray<number>)[])[]

因此,當編譯器試圖解釋一個型樣(number | number[])[]DeepArray<T>對於一些T ,它不能保證推斷number 選號完全免費number | number[] number | number[]number | (number | number[])[] number | (number | number[])[] ,或者任何類型的東西,只要它為T選擇的任何東西都允許(number | number[])[]DeepArray<T>兼容。 因此,即使flattenDeep()的實現確實壓平了值,您也不能指望“壓平” T推理:

declare const flattenDeepOrig: <T>(array: DeepArray<T>) => T[]
const oops = flattenDeepOrig([1, [2, [3, [4]], 5]]) 
// const oops: (number | (number | (number | number[])[])[])[]

當然,您可以手動指定所需的T值,編譯器將能夠檢查它是否有效,如果無效則生成錯誤:

const okayAnnotated = flattenDeepOrig<number>([1, [2, [3, [4]], 5]]) // number[]

但大概您希望編譯器為您解決這個問題。 為此,對於某些適當的FlattenArray<T>定義,使用像<T>(array: T[])=>FlattenArray<T>[]這樣的類型會更好,該定義將深度嵌套的數組類型顯式折疊為平面類型。 這是因為給定值[1, [2, [3, [4]], 5]]T[]用於唯一合理推論候選T是一樣的東西number | (number | (number | number[])[])[] number | (number | (number | number[])[])[] ,此時您希望FlattenArray<T>將其顯式展平為number 所以我們只需要定義FlattenArray<>


不幸的是, FlattenArray<T>的直接定義FlattenArray<T>編譯器不支持的方式循環:

type FlattenArrayOops<T> = T extends Array<any> ? FlattenArrayOops<T[number]> : T; // error!
//   ~~~~~~~~~~~~~~~~  <-- Type alias 'FlattenArrayOops' circularly references itself.

有一個開放的建議, microsoft/TypeScript#26980 ,允許這種循環類型,但目前沒有官方支持的方法來獲得這種行為。

在這種情況下,我通常做的是通過手動將定義“展開”到一個可以達到某個固定深度的版本來近似圓形類型,如下所示:

type FlattenArray<T> = T extends Array<any> ? FA0<T[number]> : T;
type FA0<T> = T extends Array<any> ? FA1<T[number]> : T;
type FA1<T> = T extends Array<any> ? FA2<T[number]> : T;
type FA2<T> = T extends Array<any> ? FA3<T[number]> : T;
type FA3<T> = T extends Array<any> ? FA4<T[number]> : T;
type FA4<T> = T extends Array<any> ? FA5<T[number]> : T;
type FA5<T> = T extends Array<any> ? FA6<T[number]> : T;
type FA6<T> = T extends Array<any> ? FA7<T[number]> : T;
type FA7<T> = T extends Array<any> ? FA8<T[number]> : T;
type FA8<T> = T extends Array<any> ? FAX<T[number]> : T;
type FAX<T> = T; // bail out

在這里可以看到FlattenArray<T>不是根據自身定義的,而是根據一個類似的類型FA0<T>定義的,它是根據類似的類型FA1<T>等定義的,直到達到深度FAX<T>並退出。 只要您的用例往往不是非常深的嵌套,它就應該適合您:

declare const flattenDeep: <T>(array: T[]) => FlattenArray<T>[];
const okay = flattenDeep([1, [2, [3, [4]], 5]]) // number[]

好的,希望有幫助; 祝你好運!

Playground 鏈接到代碼

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM