TypeScript: איך `ליישם` על מבנה שמכיל רשומה?


נניח שיש לנו מבנה שמחזיק רשומה שעליה אנו רוצים להחיל מבנה אחר שמחזיק רשומה שמחזיקה פונקציות עבור המפתחות של המבנה הראשון. (בעצם ההיפך מה-Apply לפי מפרט פנטזי לנד)

בואו קודם נגדיר כמה סוגים שבהם נשתמש:

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> אך עבור כל השדות המוּיפוּיים, נאמץ את הסוג שמיוצר על ידי הממפה, {[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],
}

קישור למגרש משחקים