Wix
Wix

Reputation: 2214

Typescript Sum Pluck Type

I am trying to implement a sumPluck function. It will allow the caller to specify a property of type number belonging to an object in an array then sum them.

Example:

type A = { prop: number }

const arr: A[] = [{prop: 1}, {prop: 2}];

pluckSum("prop", arr); // 3

I know how to type a pluck but I can't seem to get my type to recognize that it is actually only dealing with number properties. This is what I have:

type PropertiesOfTypeNames<T, U> = { [K in keyof T]: T[K] extends U ? K : never }[keyof T];
    type PropertiesOfType<T, U> = Pick<T, PropertiesOfTypeNames<T, U>>;

    type NumberProperties<T> = PropertiesOfType<T, number>;

    const pluckSum = <T, K extends keyof NumberProperties<T>>(arr: T[], k: K) =>
        pipe(
            arr,
            R.map(v => v[k]),
            R.sum
        );

I get an error under the map saying : Type 'T[string]' is not assignable to type 'number

So it doesn't seem that the mapped type communicates that v[k] is a number property. I must be doing something wrong here.

Upvotes: 2

Views: 304

Answers (2)

Wix
Wix

Reputation: 2214

Figured it out. I just needed to add Record like this:

const pluckSum = <T extends Record<K, number>, K extends keyof NumberProperties<T>>(arr: T[], k: K) =>
    pipe(
        arr,
        R.map(v => v[k]),
        R.sum
    );

Update 1 I guess I can further refine it by removing the mapped type "NumberProperties" since Record takes care of what I was trying to achieve anyways. So the final version is just:

const pluckSum = <T extends Record<K, number>, K extends keyof T>(k: K, arr: T[]) =>
    pipe(
        arr,
        R.map(v => v[k]),
        R.sum
    );

Upvotes: 2

user5536315
user5536315

Reputation:

Regardless of Typescript I wanted to show you an alternative approach with loop fusion, i.e. elimination of the redundant array traversal. I gain loop fusion with a somewhat unorthodox combinator of type (b -> c) -> (a -> c -> d) -> a -> b -> d (in Hindley-Milner notation), which I call contramap2nd, because it contra-maps on the second argument of a binary function.

Long story, short story:

// (b -> c) -> (a -> c -> d) -> a -> b -> d
const contramap2nd = g => f => x => y =>
  f(x) (g(y));

const arrFold = f => init => xs =>
  xs.reduce((acc, x) => f(acc) (x), init);
  
const add = x => y => x + y;
const prop = k => o => o[k];

const pluckSum = k =>
  arrFold(
    contramap2nd(
      prop(k))
        (add)) (0);

console.log(
  pluckSum("foo") ([{foo: 1}, {foo: 2}, {foo: 3}]));

You can do the contra-mapping on the second argument of add by hand, of course, if you don't like contramap2nd.

Please note that you can gain loop fusion with both map, contramap et al.

Upvotes: 2

Related Questions