DEV Community 👩‍💻👨‍💻

Ayron Wohletz
Ayron Wohletz

Posted on • Updated on • Originally published at funtoimagine.com

Getting started with functional programming in JavaScript and TypeScript

Mastering these four rules of thumb can decrease your JS/TS code volume by 5x (as argued by before/after comparison.) And your code will become easier to read, write, and maintain (as argued in Favor values over variables.)

Favor const over let

Using const instead of let forces you to stop relying on variables to get things done.

In TypeScript, you can use Readonly types to further enforce immutability. For example, the following is valid code in JavaScript. Despite the const, we can still change result:

const result = [];

for (let todo of allTodos) {
    if (!todo.completed) {
        result.push(todo);
    }
}
Enter fullscreen mode Exit fullscreen mode

But with TypeScript we can make it a compile-time error:

const result: ReadonlyArray<Readonly<Todo>> = [];

for (let todo of allTodos) {
    if (!todo.completed) {
        result.push(todo); // ERROR! Property 'push' does not exist on type 'readonly Readonly []'.
    }
}
Enter fullscreen mode Exit fullscreen mode

So I encourage using const paired with Readonly, ReadonlyArray, and the readonly keyword in TypeScript. They're like training wheels for learning to program in a functional style.

Replace loops with transformations

When you first adopt const and Readonly, you might wonder how to actually get things done. It's awkward at first, but becomes fast and fluid with practice.

The low-hanging fruit is knowing how to use map, filter, and reduce. In the above case, we can use the Array.filter method:

const incompleteTodos: ReadonlyArray<Readonly<Todo>> = allTodos.filter(todo => !todo.completed);
Enter fullscreen mode Exit fullscreen mode

If I gave a rough estimate, I would say that just this one practice of replacing common imperative loops with transformations (i.e. Array methods or Lodash functions) can reduce code volume by 5x. Here, instead of 5 lines for the for-loop, we have one line. And this is just a simple filter. More complex transformations require ever more intricate loops.

Go further along these lines, and a need will arise for a higher-level library than mere Array methods.

For one, some methods in JS mutate their objects. For example, if we wanted to sort the result array, we could use Array.sort, but that changes result. That breaks our rule of keeping values immutable. And indeed TypeScript does not allow calling sort on ReadonlyArray.

For two, some patterns and algorithms repeat often enough that we should make them their own functions. E.g. intersection, uniq, or flatMap.

This is why I recommend Lodash.

Use Lodash (or a similar library)

Lodash makes it straightforward to keep your values immutable in most cases. As you program with const, you will find many cases where Lodash has a convenient function available. E.g. for the sorting mentioned above, we could use sortBy. Or what if we wanted to display only uniquely-titled TODOs?

const incompleteTodos: ReadonlyArray<Readonly<Todo>> = allTodos.filter(todo => !todo.completed);
const uniqTodos = uniqBy(incompleteTodos, todo => todo.title);
Enter fullscreen mode Exit fullscreen mode

Lodash does have some impure functions, e.g. remove. You could try a library that enforces functional style more strictly, such as Ramda.

Even if you do use Lodash though, JavaScript still has some rough edges around its syntax in terms of functional programming. One of these is conditionals – if-statements and switches.

Replace conditionals with functions

Sadly, JS does not have conditional expressions, besides the ternary operator. Only conditional statements. So we can't do something like this:

const message = if (userLoggedIn) {
    `Hello ${user}`
} else if (userNew) {
    `Set up your account`
} else {
    `Unrecognized user`
}
Enter fullscreen mode Exit fullscreen mode

Nor can we do something like this:

const label = switch (type) {
    case "todo":
    case "task":
        "Task"
    case "activity":
        "Activity"
}
Enter fullscreen mode Exit fullscreen mode

Instead we end up with this:

let message = `Unrecognized user`

if (userLoggedIn) {
    message = `Hello ${user}`
} else if (userNew) {
    message = `Set up your account`
}
Enter fullscreen mode Exit fullscreen mode

Which of course breaks the rule of using const instead of let.

Or end up with nested ternary operators, which quickly gets unreadable:

const message = userLoggedIn ? `Hello ${user}` : (userNew ? `Set up your account` : `Unrecognized user`)
Enter fullscreen mode Exit fullscreen mode

The simple solution is to extract the conditional out into its own function:

function getUserMessage(userLoggedIn: boolean, userNew: boolean): string {
    if (userLoggedIn) {
        return `Hello ${user}`
    } else if (userNew) {
        return `Set up your account`
    } else {
        return `Unrecognized user`
    }
}

const message = getUserMessage(userLoggedIn, userNew)
Enter fullscreen mode Exit fullscreen mode

Or

function getActivityLabel(type: string): string {
    switch (type) {
        case "todo":
        case "task":
            return "Task"
        case "activity":
            return "Activity"
    }
}

const label = getActivityLabel(type)
Enter fullscreen mode Exit fullscreen mode

This preserves immutability. And has the added benefit of giving the conditional a descriptive name and making it easily unit-testable.

Summary

These rules of thumb lay a foundation for functional programming in JavaScript/TypeScript. They lead to considerable benefits in everyday code.


Updated 2/26/2022: Reduced wordiness

Top comments (0)

Visualizing Promises and Async/Await 🤯

async await

☝️ Check out this all-time classic DEV post