Skip to content

Class property is non-writable when used with decorator without initializer #7391

@yhpark

Description

@yhpark

Choose one: is this a bug report or feature request? bug report

Input Code

function Dec(target, key, descriptor) {
    // do nothing
    return descriptor;
}

class Test {
    @Dec
    a;
}

let t = new Test();

console.log(t.a); // prints 'undefined'

console.log(Object.getOwnPropertyDescriptor(t, 'a')); // writable: false (should be true)
/* prints:
{ value: undefined,
  writable: false,
  enumerable: true,
  configurable: false }
*/

t.a = 2;

console.log(t.a); // prints 'undefined' (should be 2)

Babel/Babylon Configuration (.babelrc, package.json, cli command)

{
  "presets": [
    ["@babel/preset-env", {
      "targets": {
        "node": "current"
      }
    }],
    "@babel/preset-stage-2"
  ],
  "plugins": [
    "@babel/plugin-proposal-decorators",
    [
      "@babel/plugin-proposal-class-properties",
      { "loose": true }
    ]
  ]
}

Expected Behavior

The property a should be writable.

Current Behavior

writable in a's descriptor is set to false.

Possible Solution

Currently Babel transpiles the code with _applyDecoratedDescriptor function declaration as following:

function _applyDecoratedDescriptor(target, property, decorators, descriptor, context) { var desc = {}; Object['ke' + 'ys'](descriptor).forEach(function (key) { desc[key] = descriptor[key]; }); desc.enumerable = !!desc.enumerable; desc.configurable = !!desc.configurable; if ('value' in desc || desc.initializer) { desc.writable = true; } desc = decorators.slice().reverse().reduce(function (desc, decorator) { return decorator(target, property, desc) || desc; }, desc); if (context && desc.initializer !== void 0) { desc.value = desc.initializer ? desc.initializer.call(context) : void 0; desc.initializer = undefined; } if (desc.initializer === void 0) { Object['define' + 'Property'](target, property, desc); desc = null; } return desc; }

which sets desc.writable true in the following condition:
if ('value' in desc || desc.initializer) { desc.writable = true; }
I don't understand this part. Should this be changed somehow?

Context

This behavior occurs only when both of the following conditions are met:

  • A decorator is present on the class property.
  • No initializer is defined in the class property declaration.

For example, if there's no decorator:

class Test {
    a;
}

a is writable.

Transpiled code:
function Dec(target, key, descriptor) {
  // do nothing
  return descriptor;
}

class Test {
  constructor() {
    this.a = void 0;
  }

}

let t = new Test();
console.log(t.a); // prints 'undefined'

console.log(Object.getOwnPropertyDescriptor(t, 'a')); // writable: false (should be true)

t.a = 2;
console.log(t.a); // prints 'undefined' (should be 2)

Also, if I set an initializer:

class Test {
    @Dec
    a = 1;
}

a is writable.

Transpiled code:
var _class, _descriptor;

function _initializerDefineProperty(target, property, descriptor, context) { if (!descriptor) return; Object.defineProperty(target, property, { enumerable: descriptor.enumerable, configurable: descriptor.configurable, writable: descriptor.writable, value: descriptor.initializer ? descriptor.initializer.call(context) : void 0 }); }

function _applyDecoratedDescriptor(target, property, decorators, descriptor, context) { var desc = {}; Object['ke' + 'ys'](descriptor).forEach(function (key) { desc[key] = descriptor[key]; }); desc.enumerable = !!desc.enumerable; desc.configurable = !!desc.configurable; if ('value' in desc || desc.initializer) { desc.writable = true; } desc = decorators.slice().reverse().reduce(function (desc, decorator) { return decorator(target, property, desc) || desc; }, desc); if (context && desc.initializer !== void 0) { desc.value = desc.initializer ? desc.initializer.call(context) : void 0; desc.initializer = undefined; } if (desc.initializer === void 0) { Object['define' + 'Property'](target, property, desc); desc = null; } return desc; }

function _initializerWarningHelper(descriptor, context) { throw new Error('Decorating class property failed. Please ensure that ' + 'proposal-class-properties is enabled and set to use loose mode. ' + 'To use proposal-class-properties in spec mode with decorators, wait for ' + 'the next major version of decorators in stage 2.'); }

function Dec(target, key, descriptor) {
  // do nothing
  return descriptor;
}

let Test = (_class = class Test {
  constructor() {
    _initializerDefineProperty(this, "a", _descriptor, this);
  }

}, (_descriptor = _applyDecoratedDescriptor(_class.prototype, "a", [Dec], {
  enumerable: true,
  initializer: function () {
    return 1;
  }
})), _class);
let t = new Test();
console.log(t.a); // prints 'undefined'

console.log(Object.getOwnPropertyDescriptor(t, 'a')); // writable: false (should be true)

t.a = 2;
console.log(t.a); // prints 'undefined' (should be 2)

Your Environment

software version(s)
Babel see below
Babylon
node
npm
Operating System
    "@babel/cli": "^7.0.0-beta.40",
    "@babel/core": "^7.0.0-beta.40",
    "@babel/plugin-proposal-class-properties": "^7.0.0-beta.40",
    "@babel/plugin-proposal-decorators": "^7.0.0-beta.39",
    "@babel/preset-env": "^7.0.0-beta.39",
    "@babel/preset-stage-2": "^7.0.0-beta.39"

Metadata

Metadata

Assignees

No one assigned

    Labels

    outdatedA closed issue/PR that is archived due to age. Recommended to make a new issue

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions