Essentially, this code represents two distinct functionalities:
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 ensures type safety by specifically allowing inputs where the elements of arr
possess number
properties at the specified property
key. Conversely, calculateNumSum()
simply adds up an array of numbers.
If you desire to combine these into a single function, it requires setting up multiple call signatures and implementations for TypeScript verification and runtime operation. For instance, an overloaded function can be written as follows:
// 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
This implementation operates as expected, determining whether property
is a string or undefined, thus executing either calculateObjSum
or calculateNumSum
.
Note that this approach is less type-safe compared to individual functions since TypeScript may struggle to validate compliance with each call signature distinctly. This increases the complexity both for the compiler and implementer, potentially impacting the caller too. Ultimately, choosing between a unified calculateSum
function and separate calculateObjSum
and
calculateNumSum</code functions is subjective. In my view, having distinct functions with singular responsibilities is preferable for clarity over merging them into one container. This consolidation makes analysis more difficult for compilers and implementers alike, although the decision rests with personal preference.</p>
<hr />
<p>Alternatively, one could consider both these functions as specific scenarios of a broader function accepting a variadic range of property keys indicating a path within array elements. For instance, <code>calculateSum(arr)
necessitates
number
elements in
arr
, while
calculateSum(arr, "a")
involves
{a?: number}
elements, and
calculateSum(arr, "a", "b", "c")
pertains to
{a?: {b?: {c?: number}}}
elements. Such a comprehensive function could be structured 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);
}
Testing this functionality demonstrates its effectiveness:
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
However, the operational intricacies hinder TypeScript validation due to potential need for type assertions or any
, and comprehension of the DeepNumberDict
utility type might present challenges for casual users. Despite this, from a conceptual perspective, it embodies a cohesive entity.
Playground link to code