Sets for unique array data

Lets say we are now trying out a new feature for our restaurant app where we let users suggest new dishes to be added to the menu. The restaurant owners have come up with their own ideas, but they want to open it up to the customers, too.

const customerDishes = [
  "Chicken Wings",
  "Fish Sandwich",
  "Beef Stroganoff",
  "Grilled Cheese",
  "Blue Cheese Salad",
  "Chicken Wings",
  "Reuben Sandwich",
  "Grilled Cheese",
  "Fish Sandwich",
  "Chicken Pot Pie",
  "Fish Sandwich",
  "Beef Stroganoff",
];

Here we have an array of the suggestions that have come in, but as you can see there are some repeating entries. Maybe multiple people liked the same thing or some people wrote the same suggestion multiple times. Regardless we want to take this data and get a list of unique dishes to consider for the restaurant.

So since we want to get just a subset of all of the entire array this is a great use case for the filter method. Try to think about how we would iterate over each element with filter and see if an array element already exists. If you can try to come up with an answer yourself...

The answer to this question is a little harder than it seems at first. We can iterate over each element, which we'll call dish and we want to check that the dish is somewhere in the array. To see whether a given element is an array, we know that we can use the method includes. But to make this comparison, we need to reference the entire array, which we can do:

customerDishes.filter((dish) => customerDishes.includes(dish));

But this may seem strange to you, and rightly so. In fact, there's a more intuitive way to get access to the array we're iterating over from within the method it's attached to.

There are additional parameters that are available in our callback function, the function passed to filter or any array method that requires a function. We after the element itself, we get the index of the array element that we're currently iterating over, which is normally called i. And since we have more than one parameter now, we need to add a set of parentheses around them:

customerDishes.filter((dish, i) => customerDishes.includes(dish));

And then finally, we get access to the array itself that's being iterated over. This is usually called arr or array. And this is what we can use as a more convenient reference instead of using customerDishes twice:

customerDishes.filter((dish, i, arr) => arr.includes(dish));

Note that we can use the index as well if we were using a less declarative array method like indexOf. So if we wanted to rewrite this condition to see if the dish existed elsewhere in the array:

// customerDishes.filter((dish, i, arr) => arr.includes(dish))
customerDishes.filter((dish, i, arr) => arr.indexOf(dish) === i);

And this would give us the same result. Now let's try running this by putting it in a variable uniqueDishes:

const uniqueDishes = customerDishes.filter(
  (dish, i, arr) => arr.indexOf(dish) === i
);
console.log(uniqueDishes);

And at last we get our unique dish names, and though that was a good exercise to learn more about array methods and the parameters we can use in their functions, that was a lot of work to just get a set of unique data from an array. Isn't there a shorter approach?

Sets

This bring us to a new data structure that was added to the language in ES2015 or ES6, called set. A set is a special object type, where each value within it only occur once. In other words, it enforces uniqueness of its elements.

We can easily create a set by saying:

new Set();

And within this Set function that creates a new set, we can immediately put new values into it. And we do so just like we would array elements--within square brackets.

new Set([]);

So let's add a simple set of numbers here, and like arrays, we could add whatever data type we like.

new Set([1, 2, 3]);

And note that order is preserved. So the way we organize these numbers when we create our set is significant.

So if we add the numbers 1, 2, 3 and three when it's initialized, we should have a set of three numbers. And we can confirm this by looking at the size of the set, which gives us a count of the elements within it:

console.log(new Set([1, 2, 3]).size);

Naturally, we get 3. But this is where the behavior of set comes into play and shows its value as a data structure. What if we were to replace 2 with 1, so we have two 1s? What do you think will happen based off of our definition of set?

If we check the size again:

console.log(new Set([1, 1, 3]).size);

We see that it's just 2, not 3. That's because set automatically tosses out any repeated value.

Let's say, however, we replaced these numbers, which are a primitive data type and used some object type instead, say an object or an array? What would our size be then?

console.log(new Set([[1], [1], [3]]).size);

It would be three, even though the first two arrays have the same elements. Why doesn't Set throw out one of them? Because Set can't compare objects by value to see whether they are equal, because they are reference types and therefore all, in a sense, unique. So be aware that set doesn't make objects with the same properties unique.

A set is very much like an array, because it doesn't include keys. We can't access it values by referencing a key or an index. To get the values of a set we have to iterate over it.

And like with object and map, we can use a for-of loop to do so. But first we have to store our created set in a variable which we'll call numbers. So we'll change our set back to its previous elements and set it equal to numbers.

const numbers = new Set([1, 2, 3]);

And iterating over it is even easier that looping over object data.

For each element, we'll put it in a const variable num for number. So for each num of numbers, we'll console log.

for (const num of numbers) {
  console.log(num);
}

And we get 1 2 and 3, exactly in the order that we placed them in our created set.

Set to give unique arrays elements

So this was a nice detour, but how is this going to help us get unique array values? Well as we've seen before, normal objects can be turned into arrays using certain methods, and this new object type, set can also be easily converted into an array.

And this is possible due to the spread operator. But wait, I thought we said that the spread operator only worked with arrays and normal objects? In fact, this is possible because as we just saw with the for of loop, the spread operator works with any iterable, which just means something that can be iterated over. To spread out an object into it's constituent elements, the requirement is that it be iterable.

So that's another power of the spread operator, converting sets into arrays.

Now we have everything we need to turn our original array into just unique elements with the help of set.

We can just put all of the array elements into set as it's being created.

new Set(customerDishes);

Conversely, if we felt a need to spread them into a new array, say we were adding even more elements, we could do so right within the created set:

new Set([...customerDishes]);

However there's no need to here. And then we can wrap the created set itself into a new array and spread all of its unique set elements into that. So now, if we console.log this new array of uniqueDishes:

const uniqueDishes = [...new Set(customerDishes)];
console.log(uniqueDishes);

We again just get all of the unique values.

Review

So whenever you need to get just the unique values of any Set of data, try to come up with a solution using Set.

And instead of having to manipulate our array using methods like filter, we can lean on these new data structures to give us the same solution with a lot less work and code.