網站搜尋

Typescript:如何在持有記錄的結構上 `apply`?


假設我們有一個結構體持有一條記錄,我們希望對它應用另一個結構體,該結構體持有一條記錄,記錄中包含對第一個結構體鍵的函數。(基本上是《Apply》Fantasy Land 規範的反向操作)

讓我們先定義一些我們將會使用的類型:

type Index = string|number|symbol

type Struct<R extends Record<Index, any>> = {
  readonly value: R
  apOn: <B>(fab: Struct<{[K in keyof R]?: (a: R[K], k: K) => B}>) =>
    Struct<{[K in keyof R]: B}>
}

然後處理結構本身

declare function newStruct<R extends Record<Index, any>> (value: R): Struct<R>

const struct = newStruct ({a: 1, b: true})

當為 apOn 方法的參數指定一個具有相同數量鍵且鍵對應的函數具有相同返回類型的 Struct 時,前兩個 te 按預期工作。

const withFuncsa = newStruct ({
  a: (a: number) => `${a}`,
  b: (a: boolean) => `${a}`,
})
const withFuncsb = newStruct ({
  a: (a: number) => a > 0,
})

const resa = struct.apOn (withFuncsa).value // as expected {a: string, b: string}
const resb = struct.apOn (withFuncsb).value // as expected {a: boolean, b: boolean}

但是當返回類型不同時會出錯:

const withFuncsc = newStruct ({
  a: (a: number) => a > 0,
  b: (a: boolean) => `${a}`,
})

const resc = struct.apOn (withFuncsc).value
// breaks cause expect all returns type of this withFuncc function to be identical
// expected {a: boolean, b: string}

有辦法讓它支援不同的返回類型嗎?

答案#1

我們需要重新設計這個簽名,以便在計算出其所對應的鍵之前,延遲決定 B。

type Struct<R extends Record<Index, any>> = {
  readonly value: R
  apOn: <F extends {[K in keyof Partial<R>]: (a: R[K], k: K) => any}>
      (fab: Struct<F>) =>  
          Struct<Omit<R, keyof F> & {[K in keyof F]: ReturnType<F[K]>}>
}

這裡 F 是映射器的記錄。重要的是使用 [K in keyof Partial<R>]: ... 而不是 [K in keyof R]?: ...,因為我們希望確保可選修飾符不會被展平為 ... | undefined。映射器存在且是函數,或者不存在。

然後在返回類型中,我們對未映射的欄位保持不變 Omit<R, keyof F>,但是對於所有已映射的欄位,採用映射函數生成的類型,{[K in keyof F]: ReturnType<F[K]>}。

在 IntelliSense 中,中間類型看起來有點亂,但它們能完成工作:

const t: {strings: string[], bools: boolean[]} = {
    strings: [resa.a, resa.b, resc.b],
    bools: [resb.a, resb.a, resc.a],
}

遊樂場連結