Reputation: 10062
The basic array class has .map
, .forEach
, .filter
, and .reduce
, but .groupBy
i noticably absent, preventing me from doing something like
const MyComponent = (props:any) => {
return (
<div>
{
props.tags
.groupBy((t)=>t.category_name)
.map((group)=>{
[...]
})
}
</div>
)
}
I ended up implementing something myself:
class Group<T> {
key:string;
members:T[] = [];
constructor(key:string) {
this.key = key;
}
}
function groupBy<T>(list:T[], func:(x:T)=>string): Group<T>[] {
let res:Group<T>[] = [];
let group:Group<T> = null;
list.forEach((o)=>{
let groupName = func(o);
if (group === null) {
group = new Group<T>(groupName);
}
if (groupName != group.key) {
res.push(group);
group = new Group<T>(groupName);
}
group.members.push(o)
});
if (group != null) {
res.push(group);
}
return res
}
So now I can do
const MyComponent = (props:any) => {
return (
<div>
{
groupBy(props.tags, (t)=>t.category_name)
.map((group)=>{
return (
<ul key={group.key}>
<li>{group.key}</li>
<ul>
{
group.members.map((tag)=>{
return <li key={tag.id}>{tag.name}</li>
})
}
</ul>
</ul>
)
})
}
</div>
)
}
Works pretty well, but it is too bad that I need to wrap the list rather than just being able to chain method calls.
Is there a better solution?
Upvotes: 29
Views: 96699
Reputation: 8457
For new comers just use Object.groupBy.
It is now builtin into most modern runtimes.
Upvotes: 1
Reputation: 80041
I needed a version of this that didn't use any
, and since I only need to group on string values, I revised the solution @kevinrodriguez-io provided:
const groupBy = <T extends Record<string, unknown>, K extends keyof T>(
arr: readonly T[],
keyProperty: K
) =>
arr.reduce(
(output, item) => {
const key = String(item[keyProperty])
output[key] ||= []
output[key].push(item)
return output
},
{} as Record<string, T[]>
)
// Usage:
groupBy([{ foo: "bar" }], "foo")
This enforces that the keyProperty
argument is a key of T
and returns a fully typed response.
Upvotes: 0
Reputation: 1382
You can use the following code to group stuff using Typescript.
const groupBy = <T, K extends keyof any>(list: T[], getKey: (item: T) => K) =>
list.reduce((previous, currentItem) => {
const group = getKey(currentItem);
if (!previous[group]) previous[group] = [];
previous[group].push(currentItem);
return previous;
}, {} as Record<K, T[]>);
// A little bit simplified version
const groupBy = <T, K extends keyof any>(arr: T[], key: (i: T) => K) =>
arr.reduce((groups, item) => {
(groups[key(item)] ||= []).push(item);
return groups;
}, {} as Record<K, T[]>);
So, if you have the following structure and array:
type Person = {
name: string;
age: number;
};
const people: Person[] = [
{
name: "Kevin R",
age: 25,
},
{
name: "Susan S",
age: 18,
},
{
name: "Julia J",
age: 18,
},
{
name: "Sarah C",
age: 25,
},
];
You can invoke it like:
const results = groupBy(people, i => i.name);
Which in this case, will give you an object with string keys, and Person[] values.
There are a few key concepts here:
1- You can use function to get the key, this way you can use TS infer capabilities to avoid having to type the generic every time you use the function.
2- By using the K extends keyof any
type constraint, you're telling TS that the key being used needs to be something that can be a key string | number | symbol
, that way you can use the getKey function to convert Date objects into strings for example.
3- Finally, you will be getting an object with keys of the type of the key, and values of the of the array type.
Upvotes: 76
Reputation: 17422
Instead of groupby
use reduce
. Suppose product
is your array
let group = product.reduce((r, a) => {
console.log("a", a);
console.log('r', r);
r[a.organization] = [...r[a.organization] || [], a];
return r;
}, {});
console.log("group", group);
Upvotes: 8
Reputation: 10167
During the TC39 meeting of December 2021, the proposal introducing the new Array.prototype.groupBy
and Array.prototype.groupByToMap
function has reached stage 3 in the specification process.
Here's how both functions are supposed to look like according to the README linked above:
const array = [1, 2, 3, 4, 5];
// groupBy groups items by arbitrary key.
// In this case, we're grouping by even/odd keys
array.groupBy((num, index, array) => {
return num % 2 === 0 ? 'even': 'odd';
});
// => { odd: [1, 3, 5], even: [2, 4] }
// groupByToMap returns items in a Map, and is useful for grouping using
// an object key.
const odd = { odd: true };
const even = { even: true };
array.groupByToMap((num, index, array) => {
return num % 2 === 0 ? even: odd;
});
// => Map { {odd: true}: [1, 3, 5], {even: true}: [2, 4] }
While not a 100% guaranty that it will really end up in a future version of JavaScript in the form described above (there's always a chance that the proposal can be adjusted or dropped, notably for compatibility reasons), it's nevertheless a strong commitment to have this groupBy
feature offered in the standard lib soon.
By ripple effect, it also means that these functions will be also available in TypeScript.
Upvotes: 9
Reputation: 4149
A good option might be lodash
.
npm install --save lodash
npm install --save-dev @types/lodash
Just import it import * as _ from 'lodash'
and use.
Example
_.groupBy(..)
_.map(..)
_.filter(..)
Upvotes: 8
Reputation: 666
you could add the function to the array prototype in your app (note some don't recomend this: Why is extending native objects a bad practice?):
Array.prototype.groupBy = function(/* params here */) {
let array = this;
let result;
/* do more stuff here*/
return result;
};
Then create an interface in typescript like this:
.d.ts version:
interface Array<T>
{
groupBy<T>(func:(x:T) => string): Group<T>[]
}
OR in a normal ts file:
declare global {
interface Array<T>
{
groupBy<T>(func:(x:T) => string): Group<T>[]
}
}
Then you can use:
props.tags.groupBy((t)=>t.category_name)
.map((group)=>{
[...]
})
Upvotes: 9