Immutability in JavaScript is Fun! Part 2

Nancy Garg
6 min readMay 29, 2021
Image Credit: dev.to

This article is in continuation of the previous post Part 1, where we learned what is Immutability, why we need it, Immutability with Arrays, etc.

I’ll recommend reading that first if you haven’t already! Still, if you want to continue particularly on Immutability of Objects, then Keep reading!

This section covers immutability of objects and various external libraries that we can use instead of maintaining immutability ourselves.

Immutability of Objects

We saw earlier how it is not possible to make an object immutable just by assigning it to a const variable.

Objects work by reference, So any time you try to reassign, you make changes to the original data. But there are ways through which you can make your objects follow immutability-

What is Deep and Shallow Copy of objects?

Image Credit: dev.to

Shallow Copy- A shallow copy means that the key values which are object (or non-primitives) in themselves are still connected to the original variable.

Deep Copy- A deep copy means that all of the values of the new variable are copied and disconnected from the original variable.

// Shallow copyvar obj = {a:1,b:2,c:{x:'x1',y:'y1'}};
var newObj = {...obj}; // creating a shallow copy
newObj.b = 50;
console.log(obj.b); // 2
newObj.c.x = 'x30';
console.log(obj.c.x); // x30
// Deep Copyvar obj = {a:1,b:2,c:{x:'x1',y:'y1'}};
var newObj = {...obj,c:{...obj.c}}; // creating a deep copy
newObj.b = 50;
console.log(obj.b); // 2
newObj.c.x = 'x30';
console.log(obj.c.x); // x1

There are various ways that you can use to achieve immutability of Objects in JS-

1. Object.freeze() method

Object.freeze method takes an object and makes it immutable. Neither can you change the existing properties nor you can add a new one.

To check if an object is frozen, you can use Object.isFrozen() method. Object.freeze() method came with ECMAScript 5, so it does not work with older releases.

var obj = Object.freeze({
firstName: 'Nancy',
lastName: 'Garg',
address: {
state: 'Delhi'
}
});
obj.age = 22;
console.log(obj.age); // undefined
delete obj.lastName;
console.log(obj.lastName); // Garg
obj.firstName = 'Shweta';
console.log(obj.firstName); // Nancy
obj.address.state = 'Mumbai'; // allowed as it does not deep freeze

Disadvantage- Object.freeze() does not deep freeze the object created.

2. For-of loop

For making a deep copy of an object, you can use the for-in loop. Using this, we can iterate over all the enumerable properties and functions as well.

var obj = Object.freeze({
firstName: 'Nancy',
lastName: 'Garg',
address: {
state: 'Delhi'
},
getName: function() {
return this.firstName + this.lastName;
}
});var newObj = {};for (prop in obj){
newObj[prop] = obj[prop];
};
console.log(newObj); // will give the whole new object as a copy of obj including user-defined methods as well

Disadvantage- It cannot iterate over ‘non-enumerable’ properties and functions.
It doesn’t create the deep copy (you still can use recursion to create a deep copy).

3. Stringy and parse solution

Using this, we can convert an Object into a JSON string and then parse it again into an object. You can make a deep copy using this.

var obj = Object.freeze({
firstName: 'Nancy',
lastName: 'Garg',
address: {
state: 'Delhi'
},
});var strObj = JSON.stringify(obj); // convert it into a stringvar newObj = JSON.parse(strObj); // will result into a deep copy of the object

Disadvantage- It cannot copy user-defined methods to the target object and it cannot copy ‘non-enumerable’ properties as well.

4. Assign, Create and Spread operator

  • Object.assign() method
    It creates a new copy of the object with all the enumerable values
var obj = Object.freeze({
firstName: 'Nancy',
lastName: 'Garg',
address: {
state: 'Delhi'
},
});var newObj = Object.assign(obj); // creates a new copy of obj// It doesn't copy the 'non-enumerable' properties and it cannot be used to copy prototype properties and methods
  • Object.Create() method
    This method creates a new object using the existing object with its prototype.

The properties are available in the __proto__ property of the new object, pointing to the object in the prototypal chain, making properties accessible to the object.

You can check this link https://javascript.info/prototype-inheritance if you want to know more about Prototypal inheritance.

var obj = Object.freeze({
firstName: 'Nancy',
lastName: 'Garg',
address: {
state: 'Delhi'
},
});var newObj = Object.create(obj); //returns {} object// doesn't create a separate property, instead is made available to the new object using prototype chain
// It does not create a deep copy as well
  • Spread Operator
    Spread operator can be used to make a deep copy of an object, cloning them at deep levels.
const OBJ = {x: "1",
y: 2,
z: {a: 1,
b: 2,
c: {x1: 1,
x2: 2
}
}
};
// clone one level deepconst NEW_OBJ_1 = {...OBJ};
NEW_OBJ_1.z.b = 100; //mutate two levels deep
console.log(OBJ.z.b); // 100
// clone two levels deepconst NEW_OBJ_2 = {...OBJ,z:{...OBJ.z}};
NEW_OBJ_2.z.b = 100; //mutate two levels deep
console.log(OBJ.z.b); // 2
NEW_OBJ_2.z.b.x1 = 50; //mutate three levels deep
console.log(OBJ.z.b.x1); // 50
// clone three levels deepconst NEW_OBJ_2 = {...OBJ,z:{...OBJ.z,{c:{...OBJ.z.c}}}};NEW_OBJ_2.z.b.x1 = 50; //mutate three levels deep
console.log(OBJ.z.b.x1); // 1

But this is tricky, If you are not proficient enough to understand what is going on, you may generate bugs that are incredibly hard to fix.

It’s great if you can enforce the immutability of your data through spread operator if your data structure is not complicated and nested enough.

But in case data is large and with a complex structure, using this won’t be an ideal choice. It’s better to use any external JavaScript library like Immutable.js or TypeScript etc. to help you with it.

I’ll be giving a rough idea of them, but explaining them in-depth is not in the scope, will share a few resources at the bottom if you want to read further about them in particular.

5. Immutable.js

Immutable.js is the library that lets you create complex immutable data collections without much of the hassle. They work on all data types provided by JS like Arrays, Maps, Sets, Objects, etc. Any operations performed on collection created using this library will result in a new copy of the data, leaving the original collection in its actual form.

const OBJ = Immutable.List.of('one');
// creation of a list
const NEW_OBJ = OBJ.push('two');
// push doesn't change the original array, instead return a new array

6. Immutability with TypeScript

readonly Keyword-

In case you are using TypeScript, you don’t need any other library to handle the mutability problem for you. TypeScript provides a solution to that.

readonly — You can use this keyword to represent a field in an interface as readonly. After this, you cannot override its value as it will result in compile-time error.

const ARR: readonly number[] = [1,2,3,4,5];Arr[2] = 10; // It will result into compile time error

with linter-

TypeScript linter (eg, tslint-immutable ) enforce the readonly keyword in TypeScript files.

It adds additional rules to the tslint.json configuration file:

"no-var-keyword": true,
"no-let": true,
"no-object-mutation": true,
"no-delete": true,
"no-parameter-reassignment": true,
"readonly-keyword": true,
"readonly-array": true,

After this, when you run linter, you will see the errors if there is any violation of these rules.

Conclusion

We just saw a lot about immutability including how we can replace mutable operations with immutable alternatives and it’s not difficult to put it into use if you are just aware of them. I hope this article helped you learn something!

If you haven’t checked Part 1 of it already, here is the link- Part 1

Thanks for giving it a read!

Let me know what you think about it, Suggestions and feedbacks are always welcome!

Further Reading-

--

--