Implement the Array.prototype.filterGroups

Proposal: GitHub - hotequil/proposal-filter-groups: TC39 proposal to implement the Array.prototype.filterGroups.

Hi, guys!

I've created this proposal to implement Array.prototype.filterGroups. I believe this is a simple and useful method that would be great to have in JavaScript. While working on a feature, I needed to apply multiple filters or install a library just to filter arrays in a grouped way. So I decided to contribute to TC39 by proposing this new method.

I've set up a GitHub repository and published the polyfill on npm. I’d like to find a champion for this proposal. Feel free to engage with it, suggest new features, and read the full README.md.

How it works

  1. Parameters: One or more functions (callbacks) that return a boolean (similar to filter);
  2. Return: A parent array containing filtered arrays.

Success cases

// Imports omitted…

export const users: User[] = [
  { name: "Oliver", status: { name: "active" } },
  { name: "Henry", status: { name: "inactive" } },
  { name: "Thomas", status: { name: "active" } }
];

const [activeUsers, inactiveUsers] = users.filterGroups(
  user => user.status.name === "active"
);
// [
//   [
//     { name: "Oliver", status: { name: "active" } },
//     { name: "Thomas", status: { name: "active" } }
//   ],
//   [
//     { name: "Henry", status: { name: "inactive" } }
//   ]
// ]

export const vehicles: Vehicle[] = [
  { name: "Toyota Corolla", type: "sedan" },
  { name: "Honda Fit", type: "hatch" },
  { name: "Honda Civic", type: "sedan" },
  { name: "Honda CRV", type: "suv" },
  { name: "Toyota Etios", type: "hatch" },
  { name: "Honda Odyssey", type: "van" },
  { name: "Toyota Dyna", type: "truck" },
  { name: "Toyota SW4", type: "suv" }
];

const [sedanVehicles, hatchVehicles, suvVehicles, otherVehicles] =
  vehicles.filterGroups(
    // You can use the index and array parameters too, it helps to mix many conditions
    ({ type }, _index, _array) => type === "sedan",
    vehicle => vehicle.type === "hatch",
    vehicle => vehicle.type === "suv"
  );
// [
//   [
//     { name: "Toyota Corolla", type: "sedan" },
//     { name: "Honda Civic", type: "sedan" }
//   ],
//   [
//     { name: "Honda Fit", type: "hatch" },
//     { name: "Toyota Etios", type: "hatch" }
//   ],
//   [
//     { name: "Honda CRV", type: "suv" },
//     { name: "Toyota SW4", type: "suv" }
//   ],
//   [
//     { name: "Honda Odyssey", type: "van" },
//     { name: "Toyota Dyna", type: "truck" }
//   ]
// ]

// The first callbacks have preference
const [vehiclesWithName, vehiclesWithType] =
  vehicles.filterGroups(
    ({ name }) => name.length > 0,
    ({ type }) => type.length > 0
  );
// [
//   [
//     { name: "Toyota Corolla", type: "sedan" },
//     { name: "Honda Fit", type: "hatch" },
//     { name: "Honda Civic", type: "sedan" },
//     { name: "Honda CRV", type: "suv" },
//     { name: "Toyota Etios", type: "hatch" },
//     { name: "Honda Odyssey", type: "van" },
//     { name: "Toyota Dyna", type: "truck" },
//     { name: "Toyota SW4", type: "suv" }
//   ],
//   []
// ]

Error cases

// Imports omitted…

// It'll throw a TypeError, it isn't allowed use without callbacks
vehicles.filterGroups()

// It'll throw a TypeError, it isn't allowed use numbers in callbacks
vehicles.filterGroups(
  () => true,
  () => true,
  1,
  () => true
)

// It'll throw a TypeError, it isn't allowed use strings in callbacks
vehicles.filterGroups(
  () => true,
  () => true,
  "type",
  () => true
)

// It'll throw a TypeError, it isn't allowed use booleans in callbacks
vehicles.filterGroups(
  () => true,
  () => true,
  true,
  () => true
)

// It'll throw a TypeError, it isn't allowed use objects in callbacks
vehicles.filterGroups(
  () => true,
  () => true,
  {},
  () => true
)

Performance

const numbers = Array.from(
  { length: 1_000_000 },
  (_, index) => index - 500_000
);

// 25.043ms
const [_negatives, _evens, _odds] = numbers.filterGroups(
  number => number < 0,
  number => number % 2 === 0,
  number => number % 2 !== 0
);

// 29.505ms
const _negatives2 = numbers.filter(number => number < 0);
const _evens2 = numbers.filter(number => number >= 0 && number % 2 === 0);
const _odds2 = numbers.filter(number => number >= 0 && number % 2 !== 0);

Install the polyfill

// Use your favorite package manager

npm install @hotequil/proposal-filter-groups

Import the polyfill

// Import it in your index

import "@hotequil/proposal-filter-groups"
1 Like

Hi @hotequil

It's best if proposals initially focus more on the motivation and use case. Making it clear what problem is being solved. This is more informative than the proposed solution.

I assume the "problem" here is wanting to avoid looping over the same array multiple times? So it primarily motivated by performance?

Why not write:

const negatives = [], evens = [], odds = []; 
for const (number of numbers) {
  (number < 0) && negatives.push(number);
  (number % 2 === 0)
    ? evens.push(number)
    : odds.push(number);
}

This avoids creating an extra container for the results, and avoids creating multiple functions. It also avoids duplicating the even/odd calculation. It also keeps the conditions and the name of the resulting array close together instead of replying on order, which can be error prone and harder to read.

1 Like

This looks like the use-case that Object.groupBy() solves. Here's how you would solve each of those "success cases" examples using Object.groupBy().

const { active: activeUsers, inactive: inactiveUsers } = Object.groupBy(
  users,
  user => user.status.name,
);

const { sedan, hatch, suv, other } = Object.groupBy(
  vehicles,
  ({ type }) => ['sedan', 'hatch', 'suv'].includes(type) ? type : 'other',
);

const { vehiclesWithName, vehiclesWithType } = Object.groupBy(
  vehicles,
  ({ name, type }) => {
    if (name.length > 0) return 'vehiclesWithName';
    if (type.length > 0) return 'vehiclesWithType';
    return 'other';
  }
);
4 Likes

Hi, @aclaymore!

The motivation is avoid looping the same array multiple times or use an unique callback for different conditions.

I’ll improve my proposal, thank you for helping!

Hi, @theScottyJam!

I like the Object.groupBy approach, but when the callback contains many conditions, readability can suffer. Additionally, Object.groupBy requires you to include a final condition to capture the rest of the groups. It does not order the grouped lists by the conditions, since it returns an object.

I actually came to the opposite conclusion. Say there's 10 conditions. In your proposal, that translates to 10 different callbacks. Say one of the conditions was user.age >= 18. If I wanted to figure out which destructured variable held the results for that condition, I would have to count the callbacks to see which index it was, then count the variables in the destructured array to find the matching one.

On the other hand, with Object.groupBy(), the condition would look like if (user.age >= 18) return ‘adult’; and I would just have to scan the destructured object to find where “adult” is. No counting necessary.

Promise.all() suffers from a very similar issue - you have to count to figure out which promise maps to which variable, which is why GitHub - tc39/proposal-await-dictionary: A proposal to add Promise.ownProperties(), Promise.fromEntries() to ECMAScript is being proposed.

In this case you are right, my proposal always returns the array in the same position as the callback passed to Array.prototype.filterGroups.

Promise.all absolutely can never return in a different order.