Deprecating and Removing JavaScript Object Properties

When creating an application in most languages, it’s very common to store a series of possible values in an object or as class constants. This is especially useful if the values themselves are numbers – it’s far easier to work with numbers, but considerably easier to remember names (strings). If the technique is less common in JavaScript then I believe that’s mainly because the JavaScript community is still finding its feet when it comes to application creating and creating class constants is trickier than it needs to be – if you’re already using this technique in your JavaScript applications, give yourself a pat on the back.

The benefits of keeping values in an object include the ability to change them later on (re-order them, convert them from numbers into strings etc) and you create a simple reference to all possible (or, at least, expected) values. For an example, imagine you’re writing an application which manages tasks and each task has a state.

var taskStatus = {
    NEW: 1,
    SCHEDULED: 2,
    PROGRESS: 4,
    RUNNING: 8,
    PAUSED: 16,
    HOLD: 32,
    COMPLETE: 64,
    // ...
};

Deprecating Properties

Let’s suppose that as you’re looking at the application code, you realise that taskStatus.NEW and taskStatus.SCHEDULED actually mean the same thing – there may have been a significant difference once, but now it’s just two ways of expressing the same condition. You notice that taskStatus.SCHEDULED makes more sense in your application’s context and is ued more frequently so you want to deprecate taskStatus.NEW. The question is: what is the best way to do that?

Documentation?

Assuming that you’ve been documenting your application, you can always flag the property:

/**
 * Statuses of the tasks.
 *
 * @namespace
 */
var taskStatus = {

    /**
     * The task has never been started.
     *
     * @deprecated
     * @constant
     * @type {Number}
     */
    NEW: 1,

    /**
     * The task is ready to be started.
     *
     * @constant
     * @type {Number}
     */
    SCHEDULED: 2,

    // ...

};

Although this is prudent, it doesn’t prevent a developer from writing a new part of the application that uses the status taskStatus.NEW when they should really use taskStatus.SCHEDULED. Let’s face it, someone writing documentation is rare and reading that documentation is even rarer.

Duplicate Values?

You can set the value of taskStatus.NEW to the same as taskStatus.SCHEDULED, making one an alias of the other. This can either be done within the same object creation or afterwards:

var taskStatus = {
    NEW: 2,
    SCHEDULED: 2,
    PROGRESS: 4,
    // ...
};

// or

var taskStatus = {
    SCHEDULED: 2,
    PROGRESS: 4,
    // ...
};
taskStatus.NEW = taskStatus.SCHEDULED;

This has the advantage of creating the same state when a developer uses either taskStatus.NEW or taskStatus.SCHEDULED but it has the disadvantage that it isn’t obvious that taskStatus.NEW shouldn’t be used (meaning there will probably be more cases of taskStatus.NEW appearing in the code). Other disadvantages include the first alias example looking like a mistake (that might be “corrected” by someone) and JSDoc not having a way to describe a duplicate.

Getters!

By far the best solution is to use a getter. Getters are functions that are executed when the property is accessed, allowing us to flag to future developers (or even current ones) that there is a better property to use:

var taskStatus = {
    SCHEDULED: 2,
    PROGRESS: 4,
    // ...
};

/**
 * @lends taskStatus
 */
Object.defineProperties(taskStatus, {

    /**
     * The task has never been started.
     *
     * @deprecated
     * @constant
     * @type {Number}
     */
    NEW: {

        get: function () {

            console.warn(
                "taskStatus.NEW is deprecated - " +
                "use taskStatus.SCHEDULED instead"
            );

            return 1;

        }

    }

});

// Usage:

taskStatus.SCHEDULED; // -> 2
taskStatus.NEW; // -> 1
// Warns: taskStatus.NEW is deprecated ...

You can acieve the same thing without using Object.defineProperties is you prefer – just remember to filter out properties with a getter if you want to generate combined statuses.

var taskStatus = {
    get NEW() {
        console.warn("taskStatus.NEW is deprecated ...");
        return 1;
    },
    SCHEDULED: 2,
    // ...
};

No matter which syntax you use, any developer going through the application will quickly see the warning in the console and update their code.

Getters have been available since IE9 so don’t be afraid to use them.

Removing Properties

When it comes to finally removing the property, you may want to use the getter to throw an error, convincing the developer to fix their old code.

var taskStatus = {
    get NEW() {
        throw new Error("taskStatus.NEW was removed ...");
    },
    SCHEDULED: 2,
    // ...
};

The getter approach will work well, but it involves keeping the property in the code, possibly forever. It also relies on the property name being known. If you’re anything like me, a more common problem than using an old property will be accidentally writing taskStatus.SCHEDULES and wondering why it doesn’t match any of the tasks that you think it should. It turns out that JavaScript has something that can handle both situations at the same time: [Proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy). I’ve spoken about Proxy before but here’s another great use for it:

var taskStatus = new Proxy({
    SCHEDULED: 2,
    PROGRESS: 4,
    // ...
}, {

    get: function (target, name) {

        if (
            typeof name === "string"
            && !(name in target)
        ) {

            throw new ReferenceError(
                "taskStatus." + name + " does not exist"
            );

        }

        return target[name];

    }

});

// Usage:

taskStatus.SCHEDULED; // -> 2
taskStatus.NEW; // -> ReferenceError()
taskStatus.SCHEDULES; // -> ReferenceError()

The initial check to ensure that name is a string is because a couple of Symbols are accessed when creating the object and that would cause an error since a Symbol can’t be converted into a string.

We can wrap up such useful functionality in a simple function and add Object.freeze to the mix to create an object that will only allow its existing properties to be accessed. Sady, Proxy doesn’t work in IE at all but we can work around that in a function (we can’t simulate the functionality though).

var lockDown = function (object, name) {

    console.warn(
        "Your browser doesn't understand Proxy. " +
        "Automatic detection of undefined properties " +
        "cannot be activated."
    );

    return Object.freeze(object);

};

if (typeof Proxy === "function") {

    lockDown = function (object, variableName) {

        return Object.freeze(new Proxy(object, {

            get: function (target, name) {

                if (
                    typeof name === "string"
                    && !(name in target)
                ) {

                    throw new ReferenceError(
                        typeof variableName === "string"
                            ? variableName + "." + name +
                                " does not exist"
                            : "Unrecognised property " +
                                name
                    );

                }

                return target[name];

            }

        }));

    };

}

// Usage:

var taskStatus = lockDown({
    SCHEDULED: 2,
    PROGRESS: 4,
    // ...
}, "taskStatus");

taskStatus.SCHEDULED; // -> 2
taskStatus.SCHEDULES; // -> ReferenceError()
taskStatus.NEW_STATUS = "he he";
taskStatus.NEW_STATUS; // -> ReferenceError()

The useful thing about Proxy is that is still honours any getter that we put in place:

var taskStatus = lockDown({
    get NEW() {
        console.warn("taskStatus.NEW is deprecated ...");
        return 1;
    },
    SCHEDULED: 2,
    PROGRESS: 4,
    // ...
}, "taskStatus");

taskStatus.NEW; // -> 1
// Warns: taskStatus.NEW is deprecated ...
taskStatus.SCHEDULED; // -> 2
taskStatus.SCHEDULES; // -> ReferenceError()

Methods

Just to be clear, I’m talking about properties not methods. Deprecating methods is simple – just add a console.warn to the method itself:

var myApp = {

    doOld: function () {
        console.warn("myApp.doOld is deprecated ...");
        // ...
    },

    // ...

};

Trying to execute a method that doesn’t exist will already cause a TypeError – the property will evaluate to undefined and executing undefined doesn’t work:

myApp.doesNotExist();
// TypeError: mApp.doesNotExist is not a function

Just for the record, replacing the properties with methods would get around all the issues that I’ve mentioned and will work in any browser or environment:

var innerTaskStatus = {
    NEW: 1,
    SCHEDULED: 2,
    // ...
};

var taskStatus = function (status) {

    if (!innerTaskStatus[status]) {
        throw new ReferenceError("...");
    }

    return innerTaskStatus[status];

};

The downside is that the overhead of executing a function each time you want to access a status value. Given that teh vast majority of statuses being accessed will exist and these techniques are simply there to aid developers while writing their code, that may not be desirable.

Conclusion

These are the best techniques that I can think of for letting all developers know which properties can and should be used and (more importantly) which properties should not. It’s easy to implement and maintain and doesn’t have a lot of overhead. Try them yourself the next time you have an application with a lot of different states.

Leave a Reply

Your email address will not be published. Required fields are marked *