How to apply a statically typed Record of Functions to an argument in typescript

Viewed 42

I have a finite set of function factories with similar return types, each of which returns a crud operation (but could be any operation, for the sake of this question). The return type of operation is Promise, for simplicity.

I want to convert a statically typed object of function constructors into a statically typed object of returned from these constructors functions, namely:

{f1: (k: K) => (...) => Promise<...>, f2: (k: K) => (...) => Promise<...>}

should become

{f1: (...) => Promise<...>, f2: (...) => Promise<...>}

when a key K is applied.

As a more concrete example:

type K = 'a' | 'b';

const makeGet = (k: K): (id: string): Promise<DbRecord<K>> => {...}
const makeSet = (k: K): (id: string, Partial<DbRecord<K>>): Promise<void> => {...}

const fs = {get: makeGet, set: makeSet} as const;

export const makeCrud = (k: K) => mapValues(fs, (f) => f(k)); // mapValues is a function I'm looking for

So far I manage to get only a record-like return (i.e. trying lodash mapValues or fp-ts Record.map): (pseudocode) {f1: typeof f1(k) | typeof f2(k), f2: typeof f1(k) | typeof f2(k)}, and my goal is to get a more precise mapping (pseudocode) {f1: typeof f1(k), f2: typeof f2(k)}

I have a hunch fp-ts has something for this, but I can't seem to find a right tool.

Is there such, or maybe a similar mapValues that respects k=>v static relation can be written manually?

1 Answers

You can use a mapped type to define the type you're looking for from your fs value with something like:

type MappedK<T extends Record<string, (k: K) => any>> = {
  [KK in keyof T]: ReturnType<T[KK]>
};

Playground

So it should be possible to define your mapValues (I'm calling it make) function like:

function make<T extends Record<string, (k: K) => any>>(vals: T, k: K): MappedK<T> {
  return Object.keys(vals).reduce((acc, objK) => ({
    ...acc,
    [objK]: vals[objK](k),
  }), {} as MappedK<T>);
}

export const makeCrud = (k: K) => make(fs, k);

Which I believe should work and have the correct types, but internally the types are a bit handwaved. The issue is that you have to iterate the object you're passing it at some point and all of the helpers for iterating object properties have necessarily weakened types because of how typescript works.

See this GH comment for a better explanation

RE: fp-ts

One other point related to fp-ts: it wouldn't be possible to use something like fp-ts's Record or ReadonlyRecord helpers because a Record must have a uniform type. All of the types and values in a record must be uniform so you'll always end up with a large union type in the value parameter because the value type must represent any value you can get back by indexing the Record.

If you want to maintain the relationship between the keys and the type at those keys, then you must use a mapped type.

Related