You seem to have thought this out, so I don't know if my answer is an improvement. The best I can do is this:
function groupByKeys<T>(
arr: T[keyof T][],
selector: (item: T[keyof T]) => keyof T,
possibleKeys: (keyof T)[],
): ArrayProps<T>;
function groupByKeys<T>(
arr: T[keyof T][],
selector: (item: T[keyof T]) => keyof T
): Partial<ArrayProps<T>>;
function groupByKeys<T>(
arr: T[keyof T][],
selector: (item: T[keyof T]) => keyof T,
possibleKeys?: (keyof T)[],
): Partial<ArrayProps<T>> {
const ret = {} as {[K in keyof T]: T[K][]};
if (possibleKeys) {
possibleKeys.forEach(k => ret[k] = []);
}
arr.forEach(i => {
const k = selector(i);
if (!(k in ret)) {
ret[k] = [];
}
ret[k].push(i);
})
return ret;
}
So my possibleKeys parameter differs from yours in that it's an array, not an object. Presumably Object.keys(possibleKeys) on yours results in mine.
type A = { type: 'A', a_data: string };
type B = { type: 'B', b_data: string };
type C = { type: 'C', c_data: string };
type AnyABC = A | B | C;
declare const arr: AnyABC[];
Here's how you call it:
type ABCHolder = { A: A, B: B, C: C };
groupByKeys<ABCHolder>(arr, i => i.type);
or, if you absolutely care about the output having some empty arrays instead of possibly missing some keys:
groupByKeys<ABCHolder>(arr, i => i.type, ['A','B','C']);
So, this works about the same as yours, with some differences. Upsides:
No meaningless inputs; instead you specify the type parameter and it gets erased. Or you pass in the type parameter and a list of keys (which is a bit redundant), but you're not passing in any nulls.
Since you specify that type parameter it won't let you put in 'D' in possibleKeys.
You are also prevented from doing x => "hello".
Downsides:
It would be nice to not need the type parameter or indeed refer to anything like the type {A:A, B:B, C:C}, but such type inference is apparently a little much for the compiler. (A possible refactor is to pass in the actual output object as a parameter, and it will copy results into it. If you're willing to do that I might have a different implementation for you.)
Even though x => "hello" is excluded, it still allows x => 'A', which is bad because x might be of type B or C. Ideally the selector function is of the generic type <K extends keyof T>(item: T[K])=>K, which expresses the exact constraint you want... but the compiler absolutely cannot witness that x => x.type matches that, and you are forced to assert it. And since you can assert the same thing of x => 'A', you are given no actual type safefy improvement. So forget that.
So, that's what I have. Hope it's helpful; good luck!