Issue
I have a simple existing method that sums up object array by one of the properties.
const calculateSum = <T extends object, K extends keyof T>(array: T[], property : K) : number =>{
let total = 0;
if (property)
{
total = array.reduce((acc, item) => {
return acc + (+item[property]);
}, 0);
}
return total;
}
How can this method be refactored so array number[]
can be passed as well and just to total it up in this case. How to not require property
passed in this case?
Solution
Conceptually this is really two distinct functions:
function calculateObjSum<K extends PropertyKey>(
arr: (Record<string, any> & Partial<Record<K, number>>)[],
property: K
): number {
const _arr: Partial<Record<K, number>>[] = arr;
return _arr.reduce((a, v) => a + (v[property] ?? 0), 0)
}
const s1 = calculateObjSum([{ a: "abc", b: 123 }, { a: "def", b: 456 }], "b");
console.log(s1) // 579
function calculateNumSum(arr: number[]): number {
return arr.reduce((a, v) => a + v, 0)
}
const s2 = calculateNumSum([789, 123, 456]);
console.log(s2) // 1368
The calculateObjSum()
function is a somewhat more type-safe version of your example, since it will only allow inputs where arr
's elements have number
properties at the property
key. And calculateNumSum()
just adds up an array of numbers directly.
Now, if you really want to merge those into a single function, you will have to both set up something that looks like multiple call signatures (to make TypeScript happy) and multiple implementations (to make the function work at runtime). For example, we can write the following overloaded function:
// call signatures
function calculateSum<K extends PropertyKey>(
arr: (Record<string, any> & Partial<Record<K, number>>)[],
property: K
): number;
function calculateSum(arr: number[]): number;
// implementation
function calculateSum(arr: any[], property?: PropertyKey) {
if (typeof property === "string") {
return arr.reduce((a, v) => a + (v[property] ?? 0), 0);
}
return arr.reduce((a, v) => a + v, 0);
}
const s1 = calculateSum([{ a: "abc", b: 123 }, { a: "def", b: 456 }], "b");
console.log(s1) // 579
const s2 = calculateSum([789, 123, 456]);
console.log(s2) // 1368
That works as expected. The implementation checks to see if property
is a string
as opposed to undefined
, and delegates to the implementation of calculateObjSum
or calculateNumSum
depending on the result.
Note that it's somewhat less type safe than the two-function version, because TypeScript is unable to analyze that the implementation actually satisfies each call signature separately. See microsoft/TypeScript#13235 for more information. That means you have to be careful not to mess up the implementation.
It is ultimately subjective whether or not a single calculateSum
is better than a pair of calculateObjSum
and calculateNumSum
functions. My opinion is that it is preferable to have two distinct functions that each have a clear single responsibility. The single function version feels like two functions have been squashed into a single container. This makes it harder for both the TypeScript compiler and the function implementer to reason about. And I expect the same could be said for the caller. But it's up to you.
Of course, one might decide to view both of these functions as specific instances of a more general function which takes a variadic number of property keys corresponding to a path into the array elements. So calculateSum(arr)
requires arr
's elements to be number
, and calculateSum(arr, "a")
requires the elements to be {a?: number}
, and calculateSum(arr, "a", "b", "c")
requires the elements to be {a?: {b?: {c?: number}}}
. If so, you could actually write that as a single function, possibly like this:
type DeepNumberDict<K extends PropertyKey[]> =
K extends [infer K0 extends PropertyKey, ...infer KR extends PropertyKey[]] ?
Record<string, any> & { [P in K0]?: DeepNumberDict<KR> } : number;
function calculateSum<K extends PropertyKey[]>(
arr: DeepNumberDict<K>[], ...keys: K
): number {
return arr.reduce((a, v) => a + ((keys.reduce((a, k) => (a ?? {})[k], v as any)) ?? 0), 0);
}
And you can see that it works:
const s1 = calculateSum([{ a: "abc", b: 123 }, { a: "def", b: 456 }], "b");
console.log(s1) // 579
const s2 = calculateSum([789, 123, 456]);
console.log(s2) // 1368
const s3 = calculateSum([{ a: { b: { c: 1 } } }, { a: { b: { c: 2 } } }], "a", "b", "c");
console.log(s3) // 3
Again, not sure it's worth it. The function's behavior is too complicated for TypeScript to verify (so we need type assertions and/or any
), and the DeepNumberDict
utility type might be too impenetrable for the casual user. Still, conceptually, at least, it's a single thing.
Answered By - jcalz
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.