Search code examples
javascriptbabeljsecmascript-next

Javascript Convert Class Variable to Getter/Setter using Decorators


Any ideas how I can use a decorator to convert a class field into a getter/setter? Example:

class Foo:
    @accessor bar = 0;

const foo = new Foo;

Should exhibit custom behavior on say, foo.bar = 1;

I have already tried something like

function accessor(target, name, descriptor) {
    let val;

    return {
        set: function(newVal) {
            val = newVal;
            console.log("setter called");
        },
        get: function() { return val; }
    };
}

but this loses the initial value of bar = 0.


Solution

  • The class requires to maintain private property where the value will be stored.

    Since class fields aren't currently supported by decorators proposal and newer transform-decorators Babel plugin, older transform-decorators-legacy Babel plugin should be used instead.

    As transform-decorators-legacy documentation suggests, in order to provide get/set accessors for a property, initializer method and writable property should be deleted from descriptor object. Since initializer function contains initial class field value, it should be retrieved and assigned to private property:

    function accessor(classPrototype, prop, descriptor) {
      if (descriptor.initializer)
        classPrototype['_' + prop] = descriptor.initializer();
    
      delete descriptor.writable;
      delete descriptor.initializer;
    
      descriptor.get = function () { return this['_' + prop] };
      descriptor.set = function (val) { this['_' + prop] = val };
    }
    
    class Foo {
      @accessor bar = 0;
    }
    
    const foo = new Foo ;
    foo.bar = 1;
    

    Due to the way how it works, initial value (0) will be assigned to class prototype and won't trigger set accessor, while next values (1) will be assigned to class instance and will trigger set accessor.

    Since transform-decorators-legacy isn't spec-compliant, this won't work other decorator implementations, e.g. TypeScript and decorator proposal.

    A direct spec-compliant ES6 counterpart for the code above is:

    class Foo {
      get bar() { return this._bar };
      set bar(val) { this._bar = val };
    }
    
    Foo.prototype._bar = 0;