Search code examples
javascripttypescriptdefineproperty

Why does my property disapear after Object.defineProperty in decorator?


I have a decorator that casts a string number to a javascript number.

Example: "87" -> 87.

The code is quite simple:

function digit(target: any, key: string) {

  // property value
  var _val = this[key];

  // property getter
  var getter = () =>  _val;

  // property setter
  var setter = (newVal) => _val = parseInt(newVal)

  // Create new property with getter and setter
  Object.defineProperty(target, key, {
    get: getter,
    set: setter,
    enumerable: true,
    configurable: true,
  });
}

class User {

  @digit
  public id: number;
  public isActive: boolean;

  constructor(instanceData) {
    this.id = instanceData.id;
    this.isActive = instanceData.isActive;
  }
}

let user = new User({
  id: '712',
  isActive: '1'
})

console.log([user.id]) // [ 712 ] as expected
console.log(user) // Person { isActive: '1' }

Why doesn't the id field appear in the second console.log and how can I make it appear ? I can access it but it's hidden in the console.

Thanks !


Solution

  • Because a property decorator for an instance property is applied to the prototype of the class, not to any instance. That is the best you can do because no instance of User exists before you call new User(). When you just use console.log() to get debug output you only see the instance's own properties, not the ones on the prototype.

    Since all instances share a prototype, you probably don't want to do it this way:

    let user = new User({
      id: '712',
      isActive: '1'
    }) 
    let user2 = new User({
      id: '567',
      isActive: '1'
    })
    console.log(user.id) // 567!
    

    One prototype, one id value for the whole class. All User objects have the same id. Oops.


    You probably want to at least do something to instances when they are available:

    function digit(target: any, key: string) {
      // Create new property with getter and setter
      Object.defineProperty(target, key, {
        get: function () { return this._val },
        set: function (newVal) { this._val = parseInt(newVal) },
        enumerable: true,
        configurable: true,
      });
    }
    

    Notice the use of this inside the get and set methods. That puts the _val property on the instance, so you'll get distinct ones. You will still not see id via console.log(), (and you will see _val).


    There are too many ways to configure the object so that it prints out what you expect with console.log() and behaves the way you expect (one id per instance) for me to enumerate here. As one hint: you could put the property accessor on the instance by modifying the class constructor instead of trying to use a decorator. And make sure the added _val is not enumerable so it does not appear. Hope that helps; good luck!