Generic immutable objects in JavaScript

In Avers I recently made Editable objects immutable. Whenever Avers needs to change an Editable, it makes an copy of the object and freezes it. That way accidental mutations will throw an error.

The easy solution would be to add a clone method to the class. However, I'll likely have more objects which I will make immutable, so I aimed for a more generic solution that would work with all classes.

Generic clone function for JavaScript classes

function clone<T>(x: T, f: (x: T) => void): T {
    // 1. Create a new instance of the class.
    let copy = new x.constructor;

    // 2. Copy all instance properties to the copy.
    Object.assign(copy, x);

    // 3. Apply modifications to the copy.
    f(copy);

    // 4. Freeze the copy to prevent any future modifications.
    Object.freeze(copy);

    return copy;
}

This function assumes that the class is a pure data container. That means its constructor either does not take any arguments, or it simply initializes instance properties with those. The class may have functions defined on its prototype, because the prototype is preserved.

class DataContainer {
    constructor(foo, bar) {
        // OK
        this.foo = foo;
        this.bar = bar;


        // NOT OK
        this.baz = foo * bar;
        this.fun = function() {
            return foo * bar;
        }
    }

    // OK
    f(): {
        return this.foo * this.bar;
    }
}

The callback is allowed to set instance properties. It must not modify the properties in place (eg. push to an array). When a property is not a primitive value, the callback must clone it as well.

clone(obj, copy => {
    // BAD: Modify the objects in-place
    copy.array.push(1);
    copy.nested.foo = 2;

    // GOOD: Make a copy of the objects.
    copy.array  = copy.array.slice(0).push(1);
    copy.nested = clone(copy.nested, n => { n.foo = 2; });

    // OK to set primitives
    copy.string = "blah";
    copy.isGood = true;
});

Creating the copy: new x.constructor vs Object.create(x)

In step 1 I use new x.constructor to create the copy. An alternative would be Object.create. The reason for calling the constructor is to keep the prototype chain as short as possible. With frequent clones, the prototype chain would quickly grow very long, potentially impacting performance. There are no downsides to using the constructor, as it has been around since JavaScript 1.1.

Pitfalls when working with immutable objects

One easy mistake one can make when working with immutable data is that it's very easy to make (bad) decisions based on outdated information. Especially when you hold onto objects across function boundaries, it's difficult to spot the mistake. Here's a real example from Avers:

function saveEditable(h, obj) {
    if (obj.localChanges.length === 0) {
        return;
    }

    sendPatchesToServer(...).then(() => {
        modifyEditable(h, obj.objId, obj => {
            obj.localChanges = [];
        });

        // 'obj' is now outdated, its 'localChanges' remains non-empty.
        saveEditable(h, obj);
    });
}

The symptoms were that Avers kept sending the same patches over and over to the server. The problem here is that the recursive call to saveEditable is acting on outdated information, because the modifyEditable function creates a copy and leaves the original untouched. One has to be very careful to note when an object becomes outdated.