What does immutability really mean in JavaScript?

The concept of immutability is a funny one.
In one translation, it can mean unchanging — however, setting something up as a let
or var
and then not allowing it to change might as well be as good as using const
There are a lot of people who take this translation a bit too far in its application, and as a result, misunderstand the concept and depth of immutability.
In this piece, I’ll be decrypting the idea of immutability and how it is applied in JavaScript.
Let’s look at the meaning of immutability
The word immutability has its origins in Latin and means “not changeable”. Its official synonym is “changeless” and when applied to computer programming, the idea is pretty much the same.
However, this application is often oversimplified. Many developers associate immutability with the inability to change anything at all.
For example:
var cat = "Tibbers";
cat = "Tibbles";
According to the incorrect application of the concept, the above example is regarded as a violation of immutability.
It is incorrect because there is more to immutability than not changing a particular value. Immutability functions deeper than just forcing you to keep values exactly the same.
Developers often get confused because if immutability equates to “don’t change things”, then why not just use const
?
This skepticism is valid and for a long time, I questioned this logic too.
What exactly is immutability?
When it comes to object-oriented and functional programming, the concept of immutability is applied at the object level.
It’s to do with the state and how you’re not allowed to modify after it’s been created. Every language has its own system of state management and before we go any further, we need to understand exactly what a state is.
In object-oriented, a state has two parts to it — the properties and the values. The properties are generally static, meaning that they don’t change. The values, however, are expected to be dynamic and therefore changeable.
So if we were to take a step back and observe the state for it is, it is essentially the ‘shape’ and ‘appearance’ of a particular object at any given point in time. What this means that when properties are added or removed, the overall shape, and therefore state, changes.
And when this kind of change is able to occur, the object is considered mutable
The confusion with immutability often begins around here. This is because there are different levels of application. There are applications at the variable level — where const
is used.
Weak and strong immutability is a real thing
Weak and strong immutability refers to how much an object can change.
When the object’s properties are unchangeable but its values are open to doing so, this is regarded as applying weak immutability.
A complete freeze of an object, in contrast, is a hard and strong application of immutability.
This is different from const
.
How? const
is immutable from the get-go. It is also often associated and applied to primitives, that is things like String
and Boolean
. At the moment of its assignment — that’s it — nothing about it can change.
This is handy for things like hard and fast values like pi
and system of measurement conversions.
Objects, however, are created to be mutable by nature. It can be shaped and molded until the developer is satisfied enough to turn it into an immutable object.
Think of it this way — a mutable object is like wet clay. You can shape it into whatever you want. Once you put it in the kiln, the shape becomes immutable.
You can, however, still paint it. The appearance can still change.
In this phase, its immutability factor is weak. But this doesn’t disregard the fact that it is now an immutable object. The shape is now set but the re-assignment of color is still possible. Once you glaze it, that’s basically it — you can’t technically paint it and you can’t change the overall shape. At this point, it is considered strongly immutable.
Immutability in JavaScript
In JavaScript, all primitive types are weakly immutable by design.
Why?
Because the shape of undefined
, null
, boolean
, number
, bigInt
, String
and Symbol
is singular and flat. It is a single assignment and when a primitive type is initialized, there is a clear understanding of what it’s going to look like.
It’s not going to shrink, expand, or do funny things if you change the assigned value. It might change your processed output when popped into a function, but that’s a different story.
If you need it to be strongly immutable, use const
In JavaScript, custom objects are highly mutable — unless you explicitly tell it not to be.
How do you do this? You can either do it via Object.defineProperty()
or Object.freeze()
Under normal and highly mutable
circumstances, you can do this:
let cat = {};
cat.name = "Tibbers";
cat.legs = 4;
console.log(cat);
delete cat.legs;
cat.wings = 2;
console.log(cat);
With the above example, you can keep adding and removing properties, change the values left, right, and center — and nothing is going to stop you or the rogue code you accidentally create in the far, far future.
By the end of it all, your cat
object will look more like a bird than an actual creature that goes meow.
If you’re expecting your cat to jump but it ends up flying, then this can be viewed as a side effect of the mutation that’s occurred.
Now imagine this happening to an invoice processing system where line items transform in shape and cause havoc due to missing information.
Object.defineProperty()
The way Object.defineProperty()
work is that it takes three parameters — the object you want to work on, the property you’re dealing with, and the descriptor values.
In the nutshell, Object.defineProperty()
lets you define and set the mutability status of a particular object.
What this means is that you can set the object’s properties and its values to be read-only, and therefore making it strongly immutable in the process.
In code form, it looks something like this:
let teaPot = {};
Object.defineProperty(
teaPot,
'color',
{value: 'blue', writable: false}
);
console.log(teaPot);
When you console.log()
it out, you can expect something like this:\
{ color: "blue" }
There is a little bit more to Object.defineProperty()
but that is presently beyond the current scope of this piece. However, this will work just enough to get you started on the path towards immutability and reducing defects (that is, unexpected changes) due to value mutations.
However, you need to take note that it doesn’t completely set the entire state of the object. You can still add more properties to the teaPot
object. The only thing is that you can’t modify what’s already been set as non-writable.
Even if you run something like delete teaPot.color
, once the non-writable value is set, nothing can get rid of it via normal methodologies.
So, for example, this partial implementation of strong variable immutability but mutable state in JavaScript will still run. The delete
will be silently ignored.
let teaPot = {};
Object.defineProperty(
teaPot,
'color',
{value: 'blue', writable: false}
);
teaPot.age = 3;
teaPot.age = 4;
teaPot.color = 'green';
delete teaPot.color;
console.log(teaPot);
The console.log
will output — {age: 4, color: "blue"}
As a result, color
can be regarded as a strong immutable property while the object itself is still mutable.
So how do you stop the object from changing completely? You can use Object.freeze()
Object.freeze()
Object.freeze()
lets you set an object’s state with immutability. It basically ‘freezes’ your object in its current state and any attempts to change it result in the offending code being silently ignored.
Object.freeze()
takes the object you want to apply immutability to and prevents it from any future changes.
This is particularly good to use in conjunction with Object.defineProperty()
because it lets you create an object with immutability in mind and as the end goal.
For example, you may have several properties that change over a certain set of processes but needs to be set and settled into something that won’t change once it’s confirmed. Whatever the case, here’s an example of how Object.freeze()
can be used in conjunction with Object.defineProperty()
based on the previous examples above.
let teaPot = {};
Object.defineProperty(
teaPot,
'color',
{value: 'blue', writable: false}
);
teaPot.age = 3;
teaPot.age = 4;
teaPot.color = 'purple';
Object.freeze(teaPot);
teaPot.color = 'green';
teaPot.age = 5;
delete teaPot.color;
console.log(teaPot);
This will result in the following object:
{age: 4, color: "blue"}
I know it’s not the best example but you get the gist.
Final thoughts
At its simplest and weakest, immutability is applied at the shape level.
Strong immutability is when things are simply unchangeable.
When you encounter a strongly immutable object, it’s like finding a box of old photographs of your grandparents.
In that particular moment when the photo was developed, the image becomes what it is. To create change, you’ll need to make a physical copy before anything can be done to it. Changing the original will essentially destroy it and the object as it is will be forever lost.
Now the question becomes, what if the object needs to change? Sometimes it happens and that’s alright.
Rather than changing the actual original immutable object, you can use copy methodologies to recreate the object as another instance with the same data but without the immutability.
This means you now have permission to do whatever you want with your new object without impacting on any dependencies that require the original object to remain immutable.
Comments ()