wycats / javascript-decorators

2.4k stars 127 forks source link

Change a property to getter/setter with decorator? #18

Open otakustay opened 9 years ago

otakustay commented 9 years ago

Is it possible that we can change a property to a getter / setter in decorator's descriptor.initializer function?

class CustomView {
    constructor() {
        this.controls = { table: 'table' };
    }

    @bind('table')
    table
}

function bind(name) {
    return (target, key, descriptor) => {
        descriptor.initializer = () => {
             Object.defineProperty(target, key, {
                 get: function () { return this.controls[key]; }
              });
         };
    };
}

I suggest that we have a way to do that, eg. the initializer return a special value

dead-claudia commented 9 years ago

Here's an idea:

class CustomView {
    constructor() {
        this.controls = { table: 'table' };
    }

    @bind table() {}
}

function bind(name) {
    return (_, key, {configurable, enumerable, writable}) => {
        return {
            configurable, enumerable, writable,
            get() { return this.controls[key]; },
            set(val) { return (this.controls[key] = val); },
        };
    };
}

Should that work?

fragsalat commented 8 years ago

I figured out the same issue. My plan was to have an decorator which define a custom setter and getter on the decorated property. That works for me but the decorated properties aren't anymore in serialization process included. I tried it with defineProperty, returning a new descriptor object and modifying the old descriptor object.

function decorator(target, property, descriptor) {
    let val;
    return {
        set: function (value) {
            val = value;
            console.log('set', value);
        },
        get: function() {
            return val;
            console.log('get', val);
        },
        enumerable: true,
        configurable: true
    };
};

class Test {
  id = 0;

  @decorator
  test = 'something';
}

var t = new Test();
console.log(JSON.stringify(t));
t.test = 123;
console.log(JSON.stringify(t));
console.log(JSON.stringify(t.test));

The log will print

{"id":0}
set 123
{"id":0}
123

As you might see there is no test property included but accessing the test property shows it's there. This might happen because t.hasOwnProperty('test') returns false. Any idea how to fix it or how to work around it?

dead-claudia commented 8 years ago

@fragsalat Add a toJSON method returning the relevant keys. That should fix your problem. It's technically there, just JSON.stringify won't touch the prototype other than to check for a toJSON method.

rgigger commented 6 years ago

@fragsalat I tested your code and verified that it works. I don't know how to reconcile it though with the official docs on property decorators.

from https://www.typescriptlang.org/docs/handbook/decorators.html#property-decorators

Property Decorators

A Property Decorator is declared just before a property declaration. A property decorator cannot be used in a declaration file, or in any other ambient context (such as in a declare class).

The expression for the property decorator will be called as a function at runtime, with the following two arguments:

Either the constructor function of the class for a static member, or the prototype of the class for an instance member. The name of the member. NOTE  A Property Descriptor is not provided as an argument to a property decorator due to how property decorators are initialized in TypeScript. This is because there is currently no mechanism to describe an instance property when defining members of a prototype, and no way to observe or modify the initializer for a property. The return value is ignored too. As such, a property decorator can only be used to observe that a property of a specific name has been declared for a class.

championswimmer commented 6 years ago

Maybe use a private __propName key

function decorator(target, property, descriptor) {
    let val;
    return {
        set: function (value) {
            this['__'+property] = value;
            console.log('set', value);
        },
        get: function() {
            console.log('get', val);
            return this['__'+property];
        },
        enumerable: true,
        configurable: true
    };
};

class Test {
  id = 0;

  @decorator
  test = 'something';
}

var t = new Test();
console.log(JSON.stringify(t));
t.test = '111';
console.log(JSON.stringify(t));
console.log(JSON.stringify(t.test));
stephenh commented 6 years ago

@rgigger I think the confusion is that you linked to TypeScript docs, which had decorators pre-ESnext, and in their impl, they don't turn properties into Object.defineProperty, hence there is no descriptor for the decorator to mutate.

However, if you look at the latest ESnext docs, their first example is a regular property being decorated and provided the property descriptor:

https://github.com/tc39/proposal-decorators/blob/master/METAPROGRAMMING.md

(Note this entire wycats repo has been superseded by that tc39 repo.)

E.g. I'm trying to use @fragsalat's code in TypeScript, and it does not work, but I imagine you've both seen it work in ES6/ESnext.

I'm poked around for an issue for bringing TypeScript property decorators up to match ESnext, but haven't found it yet.