This post was originally published on Leocode Blog.
Introduction
Surely in your coder/software engineer/developer/professional keyboard basher career, you came across a situation when you had to sort some kind of data or UI elements. In most cases, it's pretty straightforward, but sometimes it can be a real pain. In this article, I'll try to explain how sorting works in the TypeScript and JavaScript world, show you some real-life examples and speak briefly about few popular libraries that can make sorting much easier.
Basic sorting
Let's start with the most basic example and sort the array of strings:
const words = ['Tango', 'Zulu', 'Bravo', 'Lima'];
words.sort();
// -> ['Bravo', 'Lima', 'Tango', 'Zulu']
That's the simplest way to alphabetically sort an array of strings in ascending order. What if we want to sort it from Z to A instead? We need to pass a compare function:
const words = ['Tango', 'Zulu', 'Bravo', 'Lima'];
words.sort((a, b) => {
if (b > a) return 1;
if (b < a) return -1;
return 0;
});
// -> ['Zulu', 'Tango', 'Lima', 'Bravo']
This might look confusing at first (been there) but trust me, it's actually making a lot of sense when you get it.
How the compare function works
The compare function takes two arguments - the current element and the next element. It has to return a number that will define how the elements have to be sorted. The general rule is:
- if you return a number greater than 0 - element
b
will go to the beginning (will receive a lower index number than elementa
) - if you return a number smaller than 0 - element
a
will go to the beginning (will receive a lower index number thanb
) - if you return 0 - elements will remain at the same position
With a simple number sorting, we can even go a step further and simplify the compare method:
// Ascending order
const arr = [1, -6, 8, 20, 3, 3];
arr.sort((a, b) => a - b);
// -> [-6, 1, 3, 3, 8, 20]
// Descending order
const arr = [1, -6, 8, 20, 3, 3];
arr.sort((a, b) => b - a);
// -> [20, 8, 3, 3, 1, -6]
Let's analyze the first two array elements comparison:
- in the first example, we do
1 - (-6)
which evaluates to7
- meaning elementb
should be before elementa
(-6
should be before1
) - in the second example, we do
-6 - 1
which evaluates to-7
- meaning elementa
should be before elementb
(1
should be before-6
)
Then the sort
method continues that for other numbers. How exactly? That depends on the browser. As it turns out different browsers are using different algorithms to do that. The API and the output are the same, but under the hood, they are sorting things in a slightly different manner. If you want proof, try the code below in Firefox and Chrome consoles, and look at what's being returned - it's not the same.
const words = ['Tango', 'Zulu', 'Bravo', 'Lima'];
words.sort((a, b) => {
console.log(`Comparing ${a} to ${b}`);
if (b > a) return 1;
if (b < a) return -1;
return 0;
});
If you want to dig even deeper - check out this great explanation of different sorting algorithms.
TIP
Worth having in mind: sort
is a mutable method, which means it mutates the original array. If you want to create a new array, you can spread it and then sort it like this:
const words = ['Tango', 'Zulu', 'Bravo', 'Lima'];
[...word].sort();
Sorting with [object Object] method
In previous examples, we simply compared the strings and return the correct value to sort the strings in the array. But there's another, awesome way to do that, plus it gives us additional superpowers! I'm talking about the localeCompare
method. What is it, you ask? Similar to examples before, the function will return a number as a result of comparing the strings, but it allows us to take the locale into consideration. This is especially useful with languages that have special characters, as they might have a different position in the alphabet. Let's see some examples, so it can all make more sense.
const words = ['Tango', 'Zulu', 'Bravo', 'Lima'];
words.sort((a, b) => a.localeCompare(b, 'en'));
// -> ['Bravo', 'Lima', 'Tango', 'Zulu']
Like I've mentioned before the localeCompare
return a numeric value, so if a
is before b
in the alphabet, it yields a negative value. If b
is before a
- it yields a positive value. If they are the same - it yields 0
. And we already know what does that mean for the sort
method from the How the compare function works
section.
But where are the superpowers, you ask? Let's take a look.
Locale
If you are dealing with multiple languages in your app/website, it's important to pass the locale of the current language when you use sorting, as some characters have a different position in the alphabet.
'ö'.localeCompare('z', 'de');
// returns a negative value: in German, ö sorts before z
'ö'.localeCompare('z', 'sv');
// returns a positive value: in Swedish, ö sorts after z
Options
localeCompare
has also a third argument, which is options. There are a few of them, but I'll show you two, in my opinion, most needed on a daily basis. If you want to read more about them all, I highly recommend the MDN and Tech on the net.
Casing
You can pass a caseFirst
property to the options. If you set it up as upper
it will sort the uppercase words first (when they are starting with the same letter). If you pass lower
- it will sort the lowercase ones first.
const words = ['Tango', 'tango', 'Bravo', 'lima', 'Lima'];
words.sort((a, b) => a.localeCompare(b, 'en', { caseFirst: 'upper' }));
// -> [ 'Bravo', 'Lima', 'lima', 'Tango', 'tango' ]
words.sort((a, b) => a.localeCompare(b, 'en', { caseFirst: 'lower' }));
// -> [ 'Bravo', 'lima', 'Lima', 'tango', 'Tango' ]
Numeric values
Let's say we want to sort the numeric strings - if we won't pass the numeric: true
property, they will be treated as strings and sorted like them - "101"
will go before "2"
because 1 is smaller than 2. Check the examples below for a better understanding.
const words = ['4', '1001', '30', '200'];
words.sort((a, b) => a.localeCompare(b, 'en'));
// -> [ '1001', '200', '30', '4' ]
words.sort((a, b) => a.localeCompare(b, 'en', { numeric: true }));
// -> [ '4', '30', '200', '1001' ]
Real-life examples
We've covered the theory (which I hope will give you a better understanding of how sorting works), now let's focus on real-life examples. I want to show you a few that will use just the native sort
method and also a few that will use external libraries like [lodash](https://lodash.com/)
. Let's go!
Sorting an array of objects
Let's say we have an array of objects (each object will represent one dog) and we want to alphabetically sort these objects by the breed
property. Let's see how we can do that.
// Define the interface for our objects
interface Dog {
breed: string;
name: string;
}
const dogs: Dog[] = [
{ breed: 'German Shepard', name: 'Rex' },
{ breed: 'Spaniel', name: 'Marley' },
{ breed: 'Bulldog', name: 'Thanos' },
];
// Sort based on `breed` property.
// We don't have to explicitly type `a` and `b` arguments - Typescipt will infer them from the `dogs` array
dogs.sort((a, b) => {
if (b.breed < a.breed) return 1;
if (b.breed > a.breed) return -1;
return 0;
});
// ->
// [
// {breed: 'Bulldog', name: 'Thanos'},
// {breed: 'German Shepard', name: 'Rex'},
// {breed: 'Spaniel', name: 'Marley'}
// ]
As you can see, this is very similar to regular string sorting, the only difference is that we are comparing specific properties of the objects.
TIP
It all depends on your preferences (and probably your team standards) but you can make the sorting function even shorter:
dogs.sort((a, b) => (b.breed < a.breed ? 1 : b.breed > a.breed ? -1 : 0));
Sorting based on different values
So we covered that, but what if we'll have two bulldogs on our list? We can sort them by breed
first and then by name
.
const dogs = [
{ breed: 'German Shepard', name: 'Rex' },
{ breed: 'Spaniel', name: 'Marley' },
{ breed: 'Bulldog', name: 'Thanos' },
{ breed: 'Bulldog', name: 'Atreus' },
];
// Fist sort based on `breed` then if they are the same, sort by `name` property
dogs.sort((a, b) => {
if (b.breed < a.breed) return 1;
if (b.breed > a.breed) return -1;
else {
if (b.name < a.name) return 1;
if (b.name > a.name) return -1;
return 0;
}
});
// ->
// [
// {breed: 'Bulldog', name: 'Atreus'},
// {breed: 'Bulldog', name: 'Thanos'},
// {breed: 'German Shepard', name: 'Rex'},
// {breed: 'Spaniel', name: 'Marley'}
// ]
As you can see we just add another "branch" in here - if the compared values are the same, we add another else
path that handles comparing another property on the object - name
in this case.
TIP
Sometimes your sorting functions might get fairly complicated so it makes sense to extract them to their own methods and pass them to sort
.
// Define the interface for our objects
interface Dog {
breed: string;
name: string;
}
// It's a good idea to pass the types to arguments, otherwise TS will treat them as type of `any`
const sortDogsByBreedAndName = (a: Dog, b: Dog) => {
if (b.breed < a.breed) return 1;
if (b.breed > a.breed) return -1;
else {
if (b.name < a.name) return 1;
if (b.name > a.name) return -1;
return 0;
}
};
dogs.sort(sortDogsByBreedAndName);
Sorting based on another array
Getting back to our beloved dogs, imagine we want to sort them by breed
but not alphabetically. Let's say we have a specific list of breeds that needs to be used as a reference for the sorting logic. Take a look below as it might be easier to understand with the code example:
// This array shows the order of sorting the dogs - Spaniel should be first
// and German Shepard should be last
const breedsOrder = ['Spaniel', 'Pug', 'Bulldog', 'Poodle', 'German Shepard'];
const dogs = [
{ breed: 'German Shepard', name: 'Rex' },
{ breed: 'Spaniel', name: 'Marley' },
{ breed: 'Bulldog', name: 'Thanos' },
{ breed: 'Pug', name: 'Bean' },
{ breed: 'Poodle', name: 'Axel' },
];
dogs.sort(
(a, b) => breedsOrder.indexOf(a.breed) - breedsOrder.indexOf(b.breed)
);
// ->
// [
// { breed: 'Spaniel', name: 'Marley' },
// { breed: 'Pug', name: 'Bean' },
// { breed: 'Bulldog', name: 'Thanos' },
// { breed: 'Poodle', name: 'Axel' },
// { breed: 'German Shepard', name: 'Rex' }
// ]
What happened here? The array.indexOf
method returns a number - position of the passed argument in the array. In this situation, if we compare the index of 'German Shepard' (4)
and 'Spaniel' (0)
we will receive -4
which means that 'Spaniel'
should go first in our sorting method.
External libraries
Apart from your vanilla JS/TS solutions, there are lots of external libraries that expose a nice API to makes sorting easier. Below I will show you examples from two of them - [lodash](https://lodash.com/)
and [fast-sort](https://github.com/snovakovic/fast-sort)
[object Object]
Lodash is a very popular JavaScript utility library. It has all kind of various methods that helps a lot with you daily developer tasks. It also lets you use few sorting helper functions.
sortBy
This method allows you to sort arrays of objects. The output is exactly the same as in the Sorting based on the different values
section before. As a first argument, you pass the array to sort and the second one is an array of object properties that we want to sort by (in the example below - sort first by breed, then by name if breeds are the same).
import { sortBy } from 'lodash';
const dogs = [
{ breed: 'German Shepard', name: 'Rex' },
{ breed: 'Spaniel', name: 'Marley' },
{ breed: 'Bulldog', name: 'Thanos' },
{ breed: 'Bulldog', name: 'Atreus' },
];
sortBy(dogs, ['breed', 'name']);
// ->
// [
// {breed: 'Bulldog', name: 'Atreus'}
// {breed: 'Bulldog', name: 'Thanos'},
// {breed: 'German Shepard', name: 'Rex'},
// {breed: 'Spaniel', name: 'Marley'}
// ]
orderBy
This method is almost identical to sortBy
except that it allows specifying the sort orders of the iterates. So if we want to sort by breed in ascending manner, but then by name in descending one, we can do something like this:
import { orderBy } from 'lodash';
const dogs = [
{ breed: 'German Shepard', name: 'Rex' },
{ breed: 'Spaniel', name: 'Marley' },
{ breed: 'Bulldog', name: 'Thanos' },
{ breed: 'Bulldog', name: 'Atreus' },
];
orderBy(dogs, ['breed', 'name'], ['asc', 'desc']);
// ->
// [
// {breed: 'Bulldog', name: 'Thanos'}
// {breed: 'Bulldog', name: 'Atreus'},
// {breed: 'German Shepard', name: 'Rex'},
// {breed: 'Spaniel', name: 'Marley'}
// ]
fast-sort
This is and easy to use and flexible sorting library with TypeScript support. It has a little bit different approach than lodash when it comes to the API. Let's see how we can solve the same sorting problem as we had before:
const dogs = [
{ breed: 'German Shepard', name: 'Rex' },
{ breed: 'Spaniel', name: 'Marley' },
{ breed: 'Bulldog', name: 'Thanos' },
{ breed: 'Bulldog', name: 'Atreus' },
];
// Sort ascending by breed then by name
sort(dogs).asc([(dog) => dog.breed, (dog) => dog.name]);
// ->
// [
// {breed: 'Bulldog', name: 'Atreus'}
// {breed: 'Bulldog', name: 'Thanos'},
// {breed: 'German Shepard', name: 'Rex'},
// {breed: 'Spaniel', name: 'Marley'}
// ]
// Sort ascending by breed then descending by name
sort(dogs).by([{ asc: (dog) => dog.breed }, { desc: (dog) => dog.name }]);
// ->
// [
// {breed: 'Bulldog', name: 'Thanos'}
// {breed: 'Bulldog', name: 'Atreus'},
// {breed: 'German Shepard', name: 'Rex'},
// {breed: 'Spaniel', name: 'Marley'}
//
Both of the mentioned libraries have of course much more options and possibilities. Also there are plenty of other libraries that solve the same or similar problems, just to name a few - match-sorter
, sort-array
or relevancy
. I encourage you to check their documentation and have a play - trust me, sometimes they can be a live-saver.
Summary
I hope this article will give you more confidence when it comes to sorting in Javascript and Typescript. We went from basics, through more complicated examples and finished on external libraries that can take some of the work from our shoulders. I think you are now ready to... sort it out... (dad joke-level pun intended).