0
votes

I want to implement a generic sum method via Array.prototype.reduce that sums up a specific numeric property of an entity.

sum should accept two arguments:

  • a generic entity type E
  • one of its properties restricted to a number return value

My attempt was to use conditional and mapped types here :

Type definitions:

type MeasureableEntityPropNames<E> = 
   { [K in keyof E]: E[K] extends number ? K : never }[keyof E];
type MeasurableEntity<E> = Pick<E, MeasureableEntityPropNames<E>>;

type MyEntity = {
    p1: number,
    p2: string,
    p3: Date,
    p4: number
};

data:

let entities: MyEntity[] = [
    { p1: 1, p2: "str", p3: new Date(), p4: 3 },
    { p1: 2, p2: "str2", p3: new Date(), p4: 1 }
]

sum method and invocation:

const sum = <E>(elements: MeasurableEntity<E>[], prop: keyof MeasurableEntity<E>) =>
    elements.reduce((acc: number, cur: MeasurableEntity<E>) => {
        return acc + cur[prop]; // ERROR
    }, 0);

sum<MyEntity>(entities, "p1");

Error:

Operator '+' cannot be applied to types 'number' and 'Pick[{ [K in keyof E]: E[K] extends number ? K : never; }[keyof E]]'.

Is there something wrong with the type definitions or can't typescript infer that cur[prop] is of type number? What is to change here?

Greetings

(Typescript 3.0.1)

1

1 Answers

2
votes

While your approach to constrain the key works well for the caller, inside the function Typescript will not be able to figure out that the field specified by the key will always be a number since the conditional type will not be evaluated in this case until the type parameter is known.

We can easily get around this by using the key as the generic type argument and specifying the array passed in must be a record for which the key passed in as a type parameter must be a number. Naturally we can specify an entity with more keys as the argument but the key passed in as a second parameter will have to point to a number field

const sum = <K extends string>(elements: Record<K, number>[], prop: K) =>
    elements.reduce((acc: number, cur) => {
        return acc + cur[prop];      
    }, 0);

type MyEntity = {
    p1: number
    p2: string,
    p3: Date,
    p4: number
};
let entities: MyEntity[] = [
    { p1: 1, p2: "str", p3: new Date(), p4: 3 },
    { p1: 2, p2: "str2", p3: new Date(), p4: 1 }
]
sum(entities, "p1");
sum(entities, "p2"); //Error