Skip to main content Accessibility Feedback

Immutability in JavaScript libraries

Earlier this year, I wrote about immutability with objects and arrays in vanilla JavaScript. Today, I want to talk about immutability in JS libraries.

Let’s dig in!

An example

Let’s imagine you have a library that lets you define a temperature, and then add or subtract from that temperature. It uses a constructor pattern, letting you create different instances.

let Temp = (function () {

	/**
	 * The Constructor object
	 * @param {Number} temp The temperature
	 */
	function Constructor (temp) {
		this.temp = temp;
	}

	Constructor.prototype.adjust = function (n) {
		this.temp = this.temp + n;
		return this;
	};

	return Constructor;

})();

To use it, you might do something like this.

// Define some temperatures
let california = new Temp(72);
let vermont = new Temp(54);

// Increase the temp 3 degrees
california.adjust(3);

// Decrease the temp 7 degrees
vermont.adjust(-7);

// log the temperature
// Will log 75 and 47, respectively
console.log(california.temp);
console.log(vermont.temp);

The problem

This approach works great, but whenever you adjust a temperature, you replace (or mutate) the originally defined value.

For example, lets imagine you were trying to track changes to a temperature over time.

let monday = new Temp(54);
let tuesday = monday.adjust(3);
let wednesday = tuesday.adjust(-2);

Here, monday, tuesday, and wednesday will all have the same value for the temp property: 55. The returned value from the adjust() method is always the same instance.

Someone can also override the temperature entirely by directly setting the value of the temp property.

monday.temp = 42;

Here’s a demo.

Immutable JavaScript libraries

To fix this, we can make two simple changes to our library.

First, we’ll use the Object.defineProperties() method to define our instance properties. Unless we give them the writable property with a value of true, they can be read but not overwritten.

let Temp = (function () {

	/**
	 * The Constructor object
	 * @param {Number} temp The temperature
	 */
	function Constructor (temp) {
		Object.defineProperties(this, {
			temp: {value: temp}
		});
	}

	Constructor.prototype.adjust = function (n) {
		this.temp = this.temp + n;
		return this;
	};

	return Constructor;

})();

Now, the temp property can be read but not overwritten.

// logs 54
console.log(monday.temp);

// still logs 54
monday.temp = 42;
console.log(monday.temp);

But, it also can’t be adjusted with the adjust() method.

// this won't work now, either
monday.adjust(5);

Here’s an updated demo.

To make this all work, we also need to return a new instance whenever we run the adjust() method.

let Temp = (function () {

	/**
	 * The Constructor object
	 * @param {Number} temp The temperature
	 */
	function Constructor (temp) {
		Object.defineProperties(this, {
			temp: {value: temp}
		});
	}

	Constructor.prototype.adjust = function (n) {
		let temp = this.temp + n;
		return new Constructor(temp);
	};

	return Constructor;

})();

Now, monday, tuesday, and wednesday are all unique instances with unique temp properties.

let monday = new Temp(54);
let tuesday = monday.adjust(3);
let wednesday = tuesday.adjust(-2);

Here’s one last demo.