Sorting a JavaScript array of objects by a property value

JavaScript is a dynamically-typed language, which means that any array can contain a combination of values of different types. The following is a valid array:

["everything", 18, null, 22]

There's no obvious way to sort values of different types. The built-in function Array.prototype.sort converts all values from an array into a string, and then compares those strings to determine the position of each array value. The array above would be sorted into this:

[18, 22, "everything", null]

This is not always what you want. A common scenario is sorting an array of objects based on the value of some property. For example, we want to sort this array based on the position (from lowest to highest).

const words = [
    {word: "lie", position: 4},
    {word: "the", position: 0},
    {word: "is", position: 2},
    {word: "a", position: 3},
    {word: "cake", position: 1},
]

Array.prototype.sort takes an optional parameter called compareFn. If specified, it must be a function that takes as arguments two distinct values of the array, a and b, and returns a number. If the number is positive, a will appear after b, if it is negative, a will appear before b, if it is zero, the original order will be preserved.

This lets us sort the array from above by position:

words.sort((a, b) => a.position - b.position);

It gets more complicated if we want to sort by word:

words.sort((a, b) => {
    if (a.word < b.word) return -1;
    if (a.word > b.word) return 1;
    return 0;
});

Maybe that's just me, but I find this difficult to use. It's not easy to write, in particular if you keep forgetting which order a and b must be in and wether you should return 1 or -1. It's also difficult to read. If you looked at the code above, could you immediately tell if the sort is a-z or z-a? Maybe you could, but I can't.

We can make this easier with a helper function called by (I'm certainly not the first to come up with it). We can replace the above code with this:

words.sort(by("word", "asc"));

Which reads a lot easier.

The by function can look like this:

function by(property, order = "asc") {
    return (a, b) => {
        if (a[property] > b[property]) {
            if (order === "asc") {
                return 1;
            } else {
                return -1;
            }
        } else if (a[property] < b[property]) {
            if (order === "asc") {
                return -1;
            } else {
                return 1;
            }
        }
        return 0;
    };
}

If you use TypeScript, the below ensures that the key you provide as a value for property is actually present on all array items:

function by<T>(property: keyof T, order: "asc"|"desc" = "asc"): (a: T, b: T) => number {
    return (a, b) => {
        if (a[property] > b[property]) {
            if (order === "asc") {
                return 1;
            } else {
                return -1;
            }
        } else if (a[property] < b[property]) {
            if (order === "asc") {
                return -1;
            } else {
                return 1;
            }
        }
        return 0;
    };
}