Immutability with multidimensional arrays and objects in vanilla JS

Yesterday, we learned about immutability with arrays and objects in JavaScript.

The approaches we discussed work great for simple arrays and objects. But they have some shortcomings when working with multidimensional arrays and objects.

Today, we’re going to learn how to address that.

What’s a multidimensional array or object?

A multidimensional array or object is one that has one or more nested arrays or objects as property values.

``````// A multidimensional array
let wizards = [{
color: 'brown'
}, {
name: 'Gandalf',
color: 'gray'
}];

// A multidimensional object
let movies = {
studio: 'Pixar',
films: ['Soul', 'Onward', 'Up', 'WALL-E'],
directors: ['Brad Bird', 'Pete Docter', 'Andrew Stanton'],
details: {
founded: '1986',
founders: ['Edwin Catmull', 'Alvy Ray Smith']
}
};
``````

Nested arrays and objects are not immutable

With multidimensional arrays and objects, using `Array.from()` and `Object.assign()` (or the spread operator) creates an immutable copy of the parent array or object only.

Any nested arrays or objects inside it are still mutable.

``````// Create an immutable copy of the wizards array
let wizardsCopy = Array.from(wizards);

// Update a nested property
wizards[0].druid = true;

// logs {name: "Radagast", color: "brown", druid: true}
console.log(wizardsCopy[0]);
``````

How to create immutable multidimensional arrays and objects

To get around this, we need to loop through each property in an object or array and copy it to a new one. If the property is itself an array or object, we need to repeat the process, creating a unique immutable copy of it.

Let’s create a helper function for this called `copy()`.

``````/**
* Create an immutable clone of an array or object
* @param  {*} obj The array or object to copy
* @return {*}     The clone of the array or object
*/
function copy (obj) {
// Code goes here...
}
``````

First, we want to determine what type of object the `obj` is.

The `typeof` property is shockingly inaccurate at this, so we’re going to use the `Object.prototype.toString.call()` technique to get the true object type.

If the `type` is `object` or `array`, we’ll create an immutable copy of it using some helper functions. If not, we’ll return the item as-is.

``````/**
* Create an immutable clone of an array or object
* @param  {*} obj The array or object to copy
* @return {*}     The clone of the array or object
*/
function copy (obj) {

// Get object type
let type = Object.prototype.toString.call(obj).slice(8, -1).toLowerCase();

// Return a clone based on the object type
if (type === 'object') return cloneObj();
if (type === 'array') return cloneArr();
return obj;

}
``````

Inside the `cloneObj()` helper, we’ll create a new object (`{}`) and save it to the `clone` variable.

Then, we’ll use a `for...in` loop to loop through each item in the object and assign its value to that same `key` in the `clone` object. Since the property at that `key` could itself be an array or object, however, we’re going to pass it recursively back into the `copy()` function and use that returned result.

Finally, we’ll return the `clone`.

``````/**
* Create an immutable clone of an array or object
* @param  {*} obj The array or object to copy
* @return {*}     The clone of the array or object
*/
function copy (obj) {

// Get object type
let type = Object.prototype.toString.call(obj).slice(8, -1).toLowerCase();

/**
* Create an immutable copy of an object
* @return {Object}
*/
function cloneObj () {
let clone = {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
clone[key] = copy(obj[key]);
}
}
return clone;
}

// Return a clone based on the object type
if (type === 'object') return cloneObj();
if (type === 'array') return cloneArr();
return obj;

}
``````

We’ll do something similar in the `cloneArr()` function.

We can use the `Array.map()` method to create a new array from the existing array’s values. Again, since the `item` could itself be an array or object, we’ll pass it recursively into the `copy()` function and used the returned value.

``````/**
* Create an immutable clone of an array or object
* @param  {*} obj The array or object to copy
* @return {*}     The clone of the array or object
*/
function copy (obj) {

// Get object type
let type = Object.prototype.toString.call(obj).slice(8, -1).toLowerCase();

/**
* Create an immutable copy of an object
* @return {Object}
*/
function cloneObj () {
let clone = {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
clone[key] = copy(obj[key]);
}
}
return clone;
}

/**
* Create an immutable copy of an array
* @return {Array}
*/
function cloneArr () {
return obj.map(function (item) {
return copy(item);
});
}

// Return a clone based on the object type
if (type === 'object') return cloneObj();
if (type === 'array') return cloneArr();
return obj;

}
``````

Now, we can use the `copy()` function to create immutable copies of multidimensional arrays and objects by individually copying over each item and creating a new array or object when needed.

To use the helper function, include it in your code base, then pass the array or object to copy in as an argument.

``````/**
* Create an immutable clone of an array or object
* @param  {*} obj The array or object to copy
* @return {*}     The clone of the array or object
*/
function copy () {
// The helper function code...
}

// Create an immutable copy of wizards
let immutableWizards = copy(wizards);

// Update a nested property
wizards[0].druid = true;

// logs {name: "Radagast", color: "brown"}
// Here, the copy is unaffected by changes to the original
console.log(wizardsCopy[0]);
``````

You can find an advanced version of the `copy()` function that also creates immutable copies of `Set()`, `Map()`, and function objects on the Vanilla JS Toolkit.