Typescript: Ako `aplikovať` na štruktúru obsahujúcu záznam?


Povedzme, že máme štruktúru, ktorá drží záznam, na ktorom chceme použiť inú štruktúru, ktorá drží záznam obsahujúci funkcie pre kľúče prvej štruktúry. (v podstate opačne k špecifikácii Apply fantasy Land)

Najprv si definujme niektoré typy, ktoré budeme používať:

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}>
}

potom sa pustí do samotnej štruktúry

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

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

Prvé dva te fungujú podľa očakávania, keď sa pri zadávaní argumentu metód apOn špecifikuje Struct s rovnakým počtom kľúčov, ktoré majú funkcie s rovnakým typom návratovej hodnoty.

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}

Ale zlyhá, keď sa typy návratu líšia:

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}

Existuje spôsob, ako to mať fungujúce s rôznymi návratovými typmi?

Odpoveď č. 1

Musíme prerobiť podpis tak, aby rozhodovanie o B bolo odložené, kým nebude známy kľúč, pre ktorý sa počíta.

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]>}>
}

Tu je F záznam mapperov. Je dôležité, že [K in keyof Partial<R>]: ... namiesto [K in keyof R]?: ..., pretože chceme zabezpečiť, aby sa voliteľný modifikátor nezmenil na ... | undefined. Mappere sú prítomné a sú buď funkciou, alebo neprítomné.

Potom v návratovom type ponecháme nemapované polia rovnaké Omit<R, keyof F>, ale pre všetky mapované polia vezmeme typ vytvorený mapperom, {[K in keyof F]: ReturnType<F[K]>}.

Medzi typy vyzerajú v Intellisense trošku neporiadne, ale svoju prácu zvládnu:

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

Odkaz na ihrisko