Wednesday, March 27, 2019

Templated version of JavaScript Object.assign

In a recent article I provided a revised version of the JavaScript Object.assign function that also merged nested objects.  In this article, I'm going to revise it slightly so that it only assigns attributes according to a template.  The deepAssign method is useful for merging in partial data, some of which may be nested, into a larger set of (state) data.   The templateAssign is useful for pruning the data you're going to store to only what you need / care about.

Why would you need to do this?


In GraphQL you can specify the attributes you want so probably don't need to, but in other techniques (like json:api) you get the full object which may contain way more data than you need, and if the data set it large it can consume a lot of memory unnecessarily.  In the application this function is derived from, we have a lot of data and I've seen quite a few "Out of memory" errors in our application monitoring.  So eliminating unnecessary data is valuable.

The standard Object.assign (and the Object.deepAssign) method will combine all the object attributes, but if the incoming data contains extra attributes you don't care about, it can be handy to prune it to only what you care about.  This templateAssign function will accept a template object as the first argument and only copy over attributes from the additional source objects that are defined in the template.

If this is to be used in a map-reduction system like Redux, be sure to clone the template object into a new object via Object.assign.

Example:
Let's assume we only want attribute "a" either at the top level or a nested level

> const template =  { a: undefined, deep: { a: undefined } }
> const obj1 = { a : "a", deep: { a: "a" } }
> const obj2 = { b : "b", deep: { a: "b", b: "b" } }
> const obj3 = { c : "c", deep: { b: "b", c: "c" } }

Standard assign, doesn't merge nested "deep" object, and keeps all (top-level) attributes
> Object.assign({}, obj1, obj2, obj3)
{ a: 'a', deep: { b: 'b', c: 'c' }, b: 'b', c: 'c' }

deepAssign, merges everything (top-level and nested objects)
> Object.deepAssign({}, obj1, obj2, obj3)
{ a: 'a', deep: { a: 'b', b: 'b', c: 'c' }, b: 'b', c: 'c' }

templateAssign, merges all levels, but only keeps attributes in the template
> Object.templateAssign(Object.assign({}, template), obj1, obj2, obj3)
{ a: 'a', deep: { a: 'b' } }

Here's the code:

if (!Object.prototype.templateAssign) {
    Object.prototype.templateAssign = function(...objs) {
        let target = objs.shift();
        let source = objs.shift();
        
        if (source) {
            for(const attribute in source) {
                if (attribute in source &&  typeof(source[attribute]) === "object") {
                    target[attribute] = Object.templateAssign(Object.assign({}, target[attribute] || {}), source[attribute]);
                } else if (attribute in target &&
                           source.hasOwnProperty(attribute) &&
                           source[attribute] !== undefined) {
                    target[attribute] = source[attribute];
                }
            }
        }
        if (objs.length > 0) {
            return Object.templateAssign(target, ...objs);
        } else {
            return target;
        }
    };
}

No comments: