I have a method called groupBy (adapted from this gist) that groups an array of objects based on unique key values given an array of key names. It returns an object of shape Record<string, T[]> by default, where T[] is a sub-array of the input objects. If the boolean parameter indexes is true, it instead returns Record<string, number[]>, where number[] corresponds to indexes of the input array instead of the values themselves.
I'm able to use conditional typing in the function signature to indicate that the return type changes based on the indexes parameter:
/**
* @description Group an array of objects based on unique key values given an array of key names.
* @param {[Object]} array
* @param {[string]} keys
* @param {boolean} [indexes] Set true to return indexes from the original array instead of values
* @return {Object.<string, []>} e.g. {'key1Value1-key2Value1': [obj, obj], 'key1Value2-key2Value1: [obj]}
*/
function groupBy<T>(array: T[], keys: (keyof T)[], indexes?: false): Record<string, T[]>;
function groupBy<T>(array: T[], keys: (keyof T)[], indexes?: true): Record<string, number[]>;
function groupBy<T>(
array: T[],
keys: (keyof T)[],
indexes = false,
): Record<string, T[]> | Record<string, number[]> {
return array.reduce((objectsByKeyValue, obj, index) => {
const value = keys.map((key) => obj[key]).join('-');
// @ts-ignore
objectsByKeyValue[value] = (objectsByKeyValue[value] || []).concat(indexes ? index : obj);
return objectsByKeyValue;
}, {} as Record<string, T[]> | Record<string, number[]>);
}
const foo = groupBy([{'hey': 1}], ['hey'], true)
const bar = groupBy([{'hey': 1}], ['hey'])
When using the function, the two constants declared at the end have the appropriate typing, but I see the following error if I remove the // @ts-ignore:
TS2349: This expression is not callable.
Each member of the union type
'{ (...items: ConcatArray<number>[]): number[]; (...items: (number | ConcatArray<number>)[]): number[]; } |
{ (...items: ConcatArray<T>[]): T[]; (...items: (T | ConcatArray<...>)[]): T[]; }'
has signatures, but none of those signatures are compatible with each other.
I think I understand that this is because the conditional type isn't evaluated inside the method, so TypeScript doesn't know that the array won't have mixed types. I can resolve the error by using a larger if-else block, e.g.:
if (indexes) {
return ... as Record<string, number[]>
} else {
return ... as Record<string, T[]>
}
But this requires copying the entire function in both branches with just a slight difference. Is there a more clever way to use conditional types within the function so it's not necessary to duplicate the method with just a slight difference in each branch?
Record<string, (T | number)[]like this? Does that work for you or am I missing something?