In a previous blog post we explored how to perform immutable operations to update objects using Object.assign(). This time we are going to explore how to perform immutable operations on an array of objects which is a common pattern in Redux. If you wan’t to learn more about Redux, take a look at the excellent training course “Getting Started with Redux” by Dan Abramov, the creator of the library.
To better illustrate the differences between mutable an immutable operations for arrays, we are going to perform the same basic operations (add, update and remove) using both approaches.
Before we start, make sure to setup a basic typescript environment to work with. I’m going to name my working folder “immutable-array-operations”, but feel free to name it whatever you want.
Mutable Operations
The first step of our journey is to create a file to perform all the mutable operations called “mutable-operations.ts”.
$ touch mutable-operations.ts
In our example, we are going to to be working with an array of “people”. Every element of the array is going to be an instance of a “Person” class.
// mutable-operations.ts
class Person {
constructor(
public age?: number,
public height?: number
) {}
}
let people: Person[] = [
new Person(30, 165),
new Person(25, 178)
];
Now we can define three functions to perform the array operations in a mutable fashion for adding, removing and updating the “people” array.
// mutable-operations.ts
class Person {
constructor(
public age?: number,
public height?: number
) {}
}
let addPerson = (person: Person): void => {
people.push(person);
};
let removePerson = (person: Person): void => {
let index = people.indexOf(person);
people.splice(index, 1);
};
let updatePersonAge = (person: Person, age: number): void => {
person.age = age;
}
let people: Person[] = [
new Person(30, 165),
new Person(25, 178)
];
Now that we have initial values for the people array and all of our functions to mutate it, let’s go ahead and perform some actions. We are going to first add a new person to the array, then we are going to remove the first element of the array and finally we are going to update the age of the second person in the array.
// mutable-operations.ts
class Person {
constructor(
public age?: number,
public height?: number
) {}
}
let addPerson = (person: Person): void => {
people.push(person);
};
let removePerson = (person: Person): void => {
let index = people.indexOf(person);
people.splice(index, 1);
};
let updatePersonAge = (person: Person, age: number): void => {
person.age = age;
}
let people: Person[] = [
new Person(30, 165),
new Person(25, 178)
];
console.log("**** people: initial value ****");
console.log(JSON.stringify(people, null, 2));
let newPerson = new Person(45, 183);
addPerson(newPerson);
let deletePerson = people[0];
removePerson(deletePerson);
let updatePerson = people[1];
updatePersonAge(updatePerson, 19);
console.log("**** people: final value ****");
console.log(JSON.stringify(people, null, 2));
export default people;
The last line of our code “export default people” it’s instructing Typescript that this is an independent module, otherwise the compiler will complain if we, in another file, define again functions with the same name as we are going to do.
To see the output of this code we need to run the typescript compiler in watch mode using one of the scripts defined in our “package.json”.
$ npm run tsc:watch
In a different terminal window, we can execute node to see the output of the compiled file.
$ node mutable-operations.js
>>
**** people: initial value ****
[
{
"age": 30,
"height": 165
},
{
"age": 25,
"height": 178
}
]
**** people: final value ****
[
{
"age": 25,
"height": 178
},
{
"age": 19,
"height": 183
}
]
We have mutated the array until we get the desired result but, how can we obtain the same result without mutating the array at every step?
Immutable Operations
To compare both approaches, we are going to create a new file called “immutable-operations.ts” to put the improved version of our code.
$ touch immutable-operations.ts
With some help of the new spread operator to concatenate arrays and “Object.assign()” to create copies of an object, we can perform the same operations without mutating the “people” array.
// immutable-operations.ts
class Person {
constructor(
private age?: number,
private height?: number
) {}
}
let addPerson = (people: Person[], person: Person): Person[] => {
return [
...people,
person,
];
};
let removePerson = (people: Person[], person: Person): Person[] => {
let index = people.indexOf(person);
return [
...people.slice(0, index),
...people.slice(index + 1)
];
};
let updatePersonAge = (people: Person[], person: Person, age: number): Person[] => {
let index = people.indexOf(person);
return [
...people.slice(0, index),
Object.assign({}, person, {age}),
...people.slice(index + 1)
];
}
let people: Person[] = [
new Person(30, 165),
new Person(25, 178)
];
console.log("**** people: initial value ****");
console.log(JSON.stringify(people, null, 2));
let newPerson = new Person(45, 183);
people = addPerson(people, newPerson);
let deletePerson = people[0];
people = removePerson(people, deletePerson);
let updatePerson = people[1];
people = updatePersonAge(people, updatePerson, 19);
console.log("**** people: final value ****");
console.log(JSON.stringify(people, null, 2));
export default people;
Notice that now, after executing every immutable function, we have to reassign the return value to the “people” variable.
people = addPerson(people, newPerson);
To verify that we are still getting the same result as before, we can run this code using Node.
$ node immutable-operations.js
>>
**** people: initial value ****
[
{
"age": 30,
"height": 165
},
{
"age": 25,
"height": 178
}
]
**** people: final value ****
[
{
"age": 25,
"height": 178
},
{
"age": 19,
"height": 183
}
]
Effectively, we have the same output as before.
Conclusion
Immutable array operations for adding, removing and updating elements, like the ones shown above, are the cornerstone of one of the most popular architectural pattern: Redux. This functions are called “pure” because they don’t create any side effects. We should try to write pure functions whenever possible because they are easy to test, their output only depend on their inputs.
The source code for this article can be found here.
You can also shorten this line with the spread operator for object too 🙂
Object.assign({}, person, {age})
=> {…person, age}