Issue
I'm having some difficulty typing this correct. Basically
function groupBy<T>(data: T[], key: keyof T): Partial<Record<T[keyof T], T[]>> {
return data.reduce((acc, item) => {
const group = item[key]
acc[group] = acc[group] ?? [] // Type 'T[keyof T]' cannot be used to index type 'Partial<Record<keyof T, T[]>>'
acc[group].push(item) // Object is possibly 'undefined'.
return acc
}, {} as Partial<Record<T[keyof T], T[]>>)
}
type Human = {
gender: 'female' | 'male' | 'prefer not to say'
name: string
age: number
}
const data: Human[] = [
{ name: 'John', age: 20, gender: 'male' },
{ name: 'Jane', age: 20, gender: 'female' },
{ name: 'Jack', age: 30, gender: 'male' },
{ name: 'Jane', age: 50, gender: 'female' },
];
const groupedByGenders = groupBy(data, 'gender')
// Want `groupedByGenders` to have the type:
// Partial<Record<'female' | 'male' | 'prefer not to say', Human>>
console.log(groupedByGenders.female)
console.log(groupedByGenders.male)
console.log(groupedByGenders['prefer not to say'])
// typescript should at least auto complete this, but at best it should know that it's undefined.
(Using the Partial utility type since I sometimes use string literal types instead of just string and can't be sure that all objects in the array contains at least one of the string literal type)
Solution
My suggested typing for groupBy() looks like this:
type Serializable = string | number | bigint | boolean | null | undefined;
function groupBy<T extends Record<K, Serializable>, K extends keyof T>(
data: T[], key: K
) {
return data.reduce<{ [P in `${T[K]}`]?: T[] }>((acc, item) => {
const group: `${T[K]}` = `${item[key]}`;
const arr: T[] = (acc[group] = acc[group] ?? [])
arr.push(item);
return acc;
}, {})
}
The idea is that the elements of the data array have a Serializable property at the key key, where Serializable is a union of all types that can be reasonably pressed into service as object keys when serialized as strings. Object keys in JavaScript are always strings (with the exception of symbol values which I assume you don't care about), and if you index into an object with some non-string key it will be coerced to a string.
The implementation explicitly serializes to strings with template literal interpolation, and I give the output the corresponding template literal type. For strings this will be a no-op, but it will turn numbers like 123 into "123" and booleans like false into "false", etc. I assume in practice you'll be using mostly strings and maybe numbers.
Anyway, the return type is {[P in `${T[K]}`]?: T[]}, which is equivalent to Partial<Record<T[K], T[]>> when T[K] is some subtype of string.
Let's test it out with your Human example:
const groupedByGenders = groupBy(data, 'gender')
/* const groupedByGenders: {
female?: Human[] | undefined;
male?: Human[] | undefined;
"prefer not to say"?: Human[] | undefined;
} */
Looks good. The groupedByGenders type is an object of optional properties with keys female, male, and "prefer not to say" and with values of type Human[]. So the compiler will be happy if you write this:
console.log(groupedByGenders.male?.map(h => h.name).join(", ")) // John, Jack
Answered By - jcalz
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.