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.
- References appear to change unexpectedly
- It’s hard to reason about where and why changes happen
Mysterious changes
Let’s look at an example for the first case.
12let myObj = { value: 5 }; // Create an object3let a = myObj; // Set variable 'a' to point to that object4let b = myObj; // Set variable 'b' to point to the same object5b.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; // truemyObj == 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.
12let myObj = { value: 5 };3let a = myObj;4let b = myObj;5b = { ...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; // falsemyObj == 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.
12let myObj = Object.freeze({ value: 5 });3let a = myObj;4let b = myObj;5b.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:
12let myObj = Object.freeze({ value: 5 });3let a = myObj;4let b = myObj;5b = 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 yconst 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.