What’s the deal with immutability in JavaScript?

JavaScript

The immutability is quite a buzzword lately. Today we attempt to find out why. In this article, we explain what the immutability is and how we can benefit from it. When defining immutability, we look into various aspects of it. We investigate primitive values, assignment immutability, and how to keep our data structures immutable. We also look into how we can achieve immutability using built-in features.

Defining immutability

The basic definition of immutability is to be unable to change. It is one of the core principles of functional programming. Even if we don’t aim to write code that fully follows the rules of functional programming, we can profit from diving into it.

Types immutability

The first thing to notice is that all primitive values are in their nature immutable. We can distinguish seven primitive data types: number, bigint, boolean, string, null, undefined, and symbol.

We sometimes perceive strings as arrays. That suggests that we might mutate them, but this is not the case:

The interesting thing is that JavaScript wraps primitive types in objects. An example of such is the String. We can observe this in the below example:

The wrappers are mutable, but they disappear after they are not needed anymore. Every time we access a property on a primitive value, we have a new wrapper.

The above behavior is sometimes referred to as boxing

The primitive values being immutable is the reason why all methods that operate on primitives return new values instead of mutating them.

A good example is String.prototype.toUpperCase()

Assignment immutability

It is important not to confuse value immutability with the assignment immutability.

Above, we don’t mutate the string. Instead, we assign a new value to the   variable that is a concatenation of two strings.

The above assignment would fail if we were to use the   keyword. Let’s inspect the example below:

We can successfully mutate the object that the   variable holds, even though we used  to define it. This is because it only ensures the assignment immutability.

On the other hand, when using TypeScript, we can use a const assertion.

Doing the above marks all of the properties of the above object as readonly.

If you want to know more, check out TypeScript type inference with const assertions and the infer keyword

Reasons to worry about value mutability

First, let’s look into why we might consider keeping our values immutable.

Keeping our structures immutable makes any changes to the data more apparent. Doing so makes our code easier to read.

The   function doubles all of the provided numbers.

Right after writing all of the above, we know precisely how it works. Unfortunately, if the code is a lot longer and we come back to it after a few months, we might lose track of how we mutate our array. Also, such code is not very readable for other teammates.

Our   function returns a new array instead of modifying a new one. Since we now don’t perform a side effect, we can consider the function pure.

If you want to know more about pure functions and side effects, check out Improving our code with pure functions

Also, notice that the name of the function differs. Calling it  suggests that we now get a new array, instead of modifying the old one.

JavaScript has a lot of methods that operate on data without mutating it.

The above approach really shines in terms of readability when chaining multiple operations.

Putting JavaScript aside for a second, immutability might prove to be useful when avoiding race conditions while performing multi-threaded operations.

On the other hand, Node.js makes an attempt of creating memory shared between threads using a SharedArrayBuffer. If you want to know more, check out Node.js TypeScript #13. Sending data between Worker Threads

Summing up, making changes to our data in an apparent way helps us avoid trouble. Also, avoiding mutating data makes our codebase easier to test.

Achieving immutability using built-in features

The first thing that comes into mind when attempting to achieve immutability is Object.freeze.

Object frozen using the above function can no longer be changed. It means that we can’t add and remove properties, or change them in any way.

The above attempt to mutate the value fails silently or results in throwing an error if we are in a strict mode.

An interesting thought is that the   mutates the data in a particular way. Instead of returning a new instance, it freezes the provided object.

Also, a very important thing is that the immutability we achieve above is shallow.

If we would like to achieve deep immutability with  Object.freeze, we would need to traverse the whole structure manually in a loop.

Object.seal

The Object.seal function is also somewhat associated with immutability. It does provide it to a lesser extent, though.

The above function seals an object by preventing us from adding or removing properties. We can still change existing properties.

Summary

The immutability of values is not about not changing them at all. It is more about tracking those changes in a more structured, dependable manner and therefore being more confident in our code. While there are some libraries and native features that can help us in ensuring immutability, we might just be better off treating our data structures as immutable.

All of the advantages mentioned in the above article seem worth pursuing. Unfortunately, declaring new values instead of mutating existing ones might impact performance. We need to consider multiple things. For example, if you create said structures just a few times throughout the life of your application, the impact almost certainly is not a concern. If you do it very frequently, you might want to reconsider your approach if you jump into performance issues. You might also want to look into libraries like Immutable.js.

Approaching our code with avoiding the mutation of data might be very useful, but we need to consider the context of our application. Just as other disciplines such as Test-driven Development, static type analysis, and Object-Oriented Programming, it has its uses. We, as the programmers, are responsible for making the decision based on the pros and cons.

avatar
  Subscribe  
Notify of