TypeOfNaN

Roll Your Own JavaScript Immutability Function Using the Proxy Object

Nick Scialli
April 16, 2020

Deep immutabililty screenshot

While JavaScript allows us to mutate objects, we might choose to not allow ourselves (and fellow programmers) to do so. One of the best examples of this in the JavaScript world today is when we’re setting state in a React application. If we mutate our current state rather than a new copy of our current state, we can encounter hard-to-diagnose issues.

In this post, we roll our own immutable proxy function to prevent object mutation!

What is Object Mutation?

As a quick refresher, object mutation is when we change a property on an object or array. This is very different from reassignment, in which we point a different object reference altogether. Here are a couple examples of mutation vs. reassignment:

// Mutation
const person = { name: 'Bo' };
person.name = 'Jack';

// Reassignment
let pet = { name: 'Daffodil', type: 'dog' };
pet = { name: 'Whiskers', type: 'cat' };

And we have to keep in mind this applies to arrays as well:

// Mutation
const people = ['Jack', 'Jill', 'Bob', 'Jane'];
people[1] = 'Beverly';

// Reassignment
let pets = ['Daffodil', 'Whiskers', 'Ladybird'];
pets = ['Mousse', 'Biscuit'];

An Example of Unintended Consequences of Object Mutation

Now that we have an idea of what mutation is, how can mutation have unintended consequences? Let’s look at the following example.

const person = { name: 'Bo' };
const otherPerson = person;
otherPerson.name = 'Finn';

console.log(person);
// { name: "Finn" }

Yikes, that’s right! Both person and otherPerson are referencing the same object, so if we mutate name on otherPerson, that change will be reflected when we access person.

Instead of letting ourselves (and our fellow developers on our project) mutate an object like this, what if we threw an error? That’s where our immutable proxy solution comes in.

Our Immutable Proxy Solution

The JavaScript Proxy object is a handy bit of meta programming we can use. It allows us to wrap an object with custom functionality for things like getters and setters on that object.

For our immutable proxy, let’s create a function that takes an object and returns a new proxy for that object. when we try to get a property on that object, we check if that property is an object itself. If so, then, in recursive fashion, we return that property wrapped in an immutable proxy. Otherwise, we just return the property.

When we try to set the proxied object’s value, simple throw an error letting the user know they can’t set a property on this object.

Here’s our immutable proxy function in action:

const person = {
  name: 'Bo',
  animals: [{ type: 'dog', name: 'Daffodil' }],
};

const immutable = (obj) =>
  new Proxy(obj, {
    get(target, prop) {
      return typeof target[prop] === 'object'
        ? immutable(target[prop])
        : target[prop];
    },
    set() {
      throw new Error('Immutable!');
    },
  });

const immutablePerson = immutable(person);

const immutableDog = immutablePerson.animals[0];

immutableDog.type = 'cat';
// Error: Immutable!

And there we have it: we’re unable to mutate a property on an immutable object!

Should I Use This In Production?

No, probably not. This kind of exercise is awesome academically, but there are all sorts of awesome, robust, and well-tested solutions out there that do the same thing (e.g., ImmutableJS and ImmerJS). I recommend checking out these awesome libraries if you’re looking to include immutable data structures in your app!

If you'd like to support this blog by buying me a coffee I'd really appreciate it!

Nick Scialli

Nick Scialli is a senior UI engineer at Microsoft.

© 2024 Nick Scialli