Level up your .filter game
.filter
is a built-in array iteration method that accepts a predicate which is called against each of its values, and returns a subset of all values that return a truthy value.
That is a lot to unpack in one statement! Let’s take a look at that statement piece-by-piece.
- “Built-in” simply means that it is part of the language—you don’t need to add any libraries to get access to this functionality.
- “Iteration methods” accept a function that are run against each item of the array. Both
.map
and.reduce
are other examples of iteration methods. - A “predicate” is a function that returns a boolean.
- A “truthy value” is any value that evaluates to
true
when coerced to a boolean. Almost all values are truthy, with the exceptions of:undefined
,null
,false
,0
,NaN
, or""
(empty string).
To see .filter
in action, let’s take a look at this array of restaurants.
const restaurants = [
{
name: "Dan's Hamburgers",
price: 'Cheap',
cuisine: 'Burger',
},
{
name: "Austin's Pizza",
price: 'Cheap',
cuisine: 'Pizza',
},
{
name: "Via 313",
price: 'Moderate',
cuisine: 'Pizza',
},
{
name: "Bufalina",
price: 'Expensive',
cuisine: 'Pizza',
},
{
name: "P. Terry's",
price: 'Cheap',
cuisine: 'Burger',
},
{
name: "Hopdoddy",
price: 'Expensive',
cuisine: 'Burger',
},
{
name: "Whataburger",
price: 'Moderate',
cuisine: 'Burger',
},
{
name: "Chuy's",
cuisine: 'Tex-Mex',
price: 'Moderate',
},
{
name: "Taquerias Arandina",
cuisine: 'Tex-Mex',
price: 'Cheap',
},
{
name: "El Alma",
cuisine: 'Tex-Mex',
price: 'Expensive',
},
{
name: "Maudie's",
cuisine: 'Tex-Mex',
price: 'Moderate',
},
];
That’s a lot of information. I’m currently in the mood for a burger, so let’s filter that array down a bit.
const isBurger = ({cuisine}) => cuisine === 'burger';
const burgerJoints = restaurants.filter(isBurger);
isBurger
is the predicate, and burgerJoints
is a new array which is a subset of restaurants. It is important to note that restaurants
remained unchanged from the .filter.
Here is a simple example of two lists being rendered—one of the original restaurants
array, and one of the filtered burgerJoints
array.
See the Pen .filter – isBurger by Adam Giese (@AdamGiese) on CodePen.
Negating Predicates
For every predicate there is an equal and opposite negated predicate.
A predicate is a function that returns a boolean. Since there are only two possible boolean values, that means it is easy to “flip” the value of a predicate.
A few hours have passed since I’ve eaten my burger, and now I’m hungry again. This time, I want to filter out burgers to try something new. One option is to write a new isNotBurger
predicate from scratch.
const isBurger = ({cuisine}) => cuisine === 'burger';
const isNotBurger = ({cuisine}) => cuisine !== 'burger';
However, look at the amount of similarities between the two predicates. This is not very DRY code. Another option is to call the isBurger
predicate and flip the result.
const isBurger = ({cuisine}) => cuisine === 'burger';
const isNotBurger = ({cuisine}) => !isBurger(cuisine);
This is better! If the definition of a burger changes, you will only need to change the logic in one place. However, what if we have a number of predicates that we’d like to negate? Since this is something that we’d likely want to do often, it may be a good idea to write a negate
function.
const negate = predicate => function() {
return !predicate.apply(null, arguments);
}
const isBurger = ({cuisine}) => cuisine === 'burger';
const isNotBurger = negate(isBurger);
const isPizza = ({cuisine}) => cuisine === 'pizza';
const isNotPizza = negate(isPizza);
You may have some questions.
What is .apply?
apply()
method calls a function with a giventhis
value, andarguments
provided as an array (or an array-like object).
What is arguments?
The
arguments
object is a local variable available within all (non-arrow) functions. You can refer to a function’s arguments within the function by using thearguments
object.
Why return an old-school function
instead of a newer, cooler arrow function?
In this case, returning a traditional function
is necessary because the arguments
object is only available on traditional functions.
Returning Predicates
As we saw with our negate
function, it is easy for a function to return a new function in JavaScript. This can be useful for writing “predicate creators.” For example, let’s look back at our isBurger
and isPizza
predicates.
const isBurger = ({cuisine}) => cuisine === 'burger';
const isPizza = ({cuisine}) => cuisine === 'pizza';
These two predicates share the same logic; they only differ in comparisons. So, we can wrap the shared logic in an isCuisine
function.
const isCuisine = comparison => ({cuisine}) => cuisine === comparison;
const isBurger = isCuisine('burger');
const isPizza = isCuisine('pizza');
This is great! Now, what if we want to start checking the price?
const isPrice = comparison => ({price}) => price === comparison;
const isCheap = isPrice('cheap');
const isExpensive = isPrice('expensive');
Now the isCheap
and isExpensive
are DRY, and isPizza
and isBurger
are DRY—but isPrice
and isCuisine
share their logic! Luckily, there are no rules for how many functions deep you can return.
const isKeyEqualToValue = key => value => object => object[key] === value;
// these can be rewritten
const isCuisine = isKeyEqualToValue('cuisine');
const isPrice = isKeyEqualToValue('price');
// these don't need to change
const isBurger = isCuisine('burger');
const isPizza = isCuisine('pizza');
const isCheap = isPrice('cheap');
const isExpensive = isPrice('expensive');
This, to me, is the beauty of arrow functions. In a single line, you can elegantly create a third-order function. isKeyEqualToValue
is a function that returns the function isPrice
which returns the function isCheap
.
See how easy it is to create multiple filtered lists from the original restaurants
array?
See the Pen .filter – returning predicates by Adam Giese (@AdamGiese) on CodePen.
Composing Predicates
We can now filter our array by burgers or by a cheap price… but what if you want cheap burgers? One option is to simply chain to filters together.
const cheapBurgers = restaurants.filter(isCheap).filter(isBurger);
Another option is to “compose” the two predicates into a single one.
const isCheapBurger = restaurant => isCheap(restaurant) && isBurger(restaurant);
const isCheapPizza = restaurant => isCheap(restaurant) && isPizza(restaurant);
Look at all of that repeated code. We can definitely wrap this into a new function!
const both = (predicate1, predicate2) => value =>
predicate1(value) && predicate2(value);
const isCheapBurger = both(isCheap, isBurger);
const isCheapPizza = both(isCheap, isPizza);
const cheapBurgers = restaurants.filter(isCheapBurger);
const cheapPizza = restaurants.filter(isCheapPizza);
What if you are fine with either pizza or burgers?
const either = (predicate1, predicate2) => value =>
predicate1(value) || predicate2(value);
const isDelicious = either(isBurger, isPizza);
const deliciousFood = restaurants.filter(isDelicious);
This is a step in the right direction, but what if you have more than two foods you’d like to include? This isn’t a very scalable approach. There are two built-in array methods that come in handy here. .every
and .some
are both predicate methods that also accept predicates. .every
checks if each member of an array passes a predicate, while .some
checks to see if any member of an array passes a predicate.
const isDelicious = restaurant =>
[isPizza, isBurger, isBbq].some(predicate => predicate(restaurant));
const isCheapAndDelicious = restaurant =>
[isDelicious, isCheap].every(predicate => predicate(restaurant));
And, as always, let’s wrap them up into some useful abstraction.
const isEvery = predicates => value =>
predicates.every(predicate => predicate(value));
const isAny = predicates => value =>
predicates.some(predicate => predicate(value));
const isDelicious = isAny([isBurger, isPizza, isBbq]);
const isCheapAndDelicious = isEvery([isCheap, isDelicious]);
isEvery
and isAny
both accept an array of predicates and return a single predicate.
Since all of these predicates are easily created by higher order functions, it isn’t too difficult to create and apply these predicates based on a user’s interaction. Taking all of the lessons we have learned, here is an example of an app that searches restaurants by applying filters based on button clicks.
See the Pen .filter – dynamic filters by Adam Giese (@AdamGiese) on CodePen.
Wrapping up
Filters are an essential part of JavaScript development. Whether you’re sorting out bad data from an API response or responding to user interactions, there are countless times when you would want a subset of an array’s values. I hope this overview helped with ways that you can manipulate predicates to write more readable and maintainable code.
The post Level up your .filter game appeared first on CSS-Tricks.