ค้นหาเว็บไซต์

Typescript: วิธีการ `apply` กับโครงสร้างที่เก็บเรคอร์ด?


ลองสมมติว่าเรามีโครงสร้างที่เก็บระเบียนซึ่งเราต้องการ 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 ที่มีจำนวนคีย์เท่ากันซึ่งมีฟังก์ชันที่มีชนิดค่าที่ส่งกลับเหมือนกัน

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> แต่สำหรับฟิลด์ที่แมปทั้งหมด ให้ใช้ชนิดที่ถูกสร้างโดย mapper, {[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],
}

ลิงก์สนามเด็กเล่น