Skip to content

Immutability

Normally when we want to change a property of an object, we assign it directly:

myObj.value = myObj.value * 2;

The object is said to mutate, since its internal properties are changing, but the myObj reference stays the same. The identity of the object is the same after its value has changed. Immutable is when the value or contents of an object never changes.

This pretty common-sense: a lot of the objects of the everyday world are mutable. If someone breaks their arm, it’s not like they become a new person.

However in code, this can lead to some confusion for two reasons.

  1. References appear to change unexpectedly
  2. It’s hard to reason about where and why changes happen

Mysterious changes

Let’s look at an example for the first case.

1
2
let myObj = { value: 5 }; // Create an object
3
let a = myObj; // Set variable 'a' to point to that object
4
let b = myObj; // Set variable 'b' to point to the same object
5
b.value = 10; // ?

On line 3 & 4 we have two variables pointing at the same object. Because the names of the variables are different, it’s easy to think that they refer to different things.

When we change b on line 5, we’d expect to just change b, but what happens is that a, b and myObj all change:

a; // { value: 10 }
b; // { value: 10 }
myObj; // { value: 10 }
a == b; // true
myObj == b; // true

This is because myObj, a and b are all the same object, they have the same identity. A naive reading of the code may give you to think there are three objects (myObj, a and b) but there’s really only one, with all three variables pointing at the same thing: a === b === myObj.

Another problem with mutability is when passing objects to a function. The function might change the object in unexpected ways, you have no assurances of what it does. When object references get used in different areas of the code all mutating the object it can very easily be confusing: ‘who’ is changing the object? Why are they? When are they? etc.

Making things immutable

Let’s revisit the simple example, but work in an immutable way.

1
2
let myObj = { value: 5 };
3
let a = myObj;
4
let b = myObj;
5
b = { ...myObj, value: 10 }

Now we aren’t changing the values of b, we are creating a new object with the spread syntax and assigning back to a.

a; // { value: 5 }
b; // { value: 10 }
myObj; // { value: 5 }
a == b; // false
myObj == b; // false

This is better! Now only b is changing, just as we’d expect from a casual look at the code.

However, at present there’s nothing to stop us from mutating the object. We could still write b.value = ...

Freeze for safety

There are discussions about adding more mechanisms for immutability in Javascript, but for now we have Object.freeze.

Give it an object, and it will give you back a ‘frozen’ version such that an error is thrown if you try to modify it.

1
2
let myObj = Object.freeze({ value: 5 });
3
let a = myObj;
4
let b = myObj;
5
b.value = 10; // Error: cannot assign to read only property

Ok, we’ve stopped ourselves from changing the object. To change it we have to reassign as before, this time making sure also freeze the new object too:

1
2
let myObj = Object.freeze({ value: 5 });
3
let a = myObj;
4
let b = myObj;
5
b = Object.freeze({ ...myObj, value: 10 });

Now we have stronger guarantees of code safety. If you’re using Typescript, this sort of thing can be expressed very easily without all the Object.freeze() going on.

It can be rather ugly to use Object.freeze() all the time, so perhaps use it for key objects that you don’t want to risk changing unexpectedly. In the ixfx demos, we use that for state and settings.

Copying immutable objects

To copy immutable objects, you can just skip assigning properties:

const myObj = { value: 5 };
const myObj2 = { ...myObj };

Comparing immutable objects

A gotcha with immutability is that objects can’t be compared using the usual equality operator (===).

const a = { value: 5 };
const b = { value: 5 };
a === b; // false - different objects even though values are the same

Instead, you need to compare by values.

// Returns _true_ if 'value' property is
// the same on both x and y
const isEqual (x, y) => x.value === y.value;
isEqual(a, b); // True!

ixfx has helper functions for comparing values.

In Typescript

If you’re using Typescript, soft immutability is easy (soft in the sense it’s not enforced at runtime).

We don’t need to use Object.freeze(), as the Typescript compiler will stop us from breaking the rules.

type MyType = Readonly<{
a: value
}>
let myObj:MyType = { value: 5 };
myObj.value = 10; // TS error, can't assign to a readonly property

The Readonly utility type can wrap most things, also arrays.