tc39/proposal-decorators

diferenciate get and set and decorator

pabloalmunia opened this issue · 8 comments

We are founding a very special and curious case with getter/setter decorators. Let's suppose we have a general decorator for methods (ie a @trace). We want to be able to apply this decorator only over the getter or the setter. Nevertheless, we don't kwon if the user programmer put our decorator over the setter or getter.

class T {
  get x() {}

  @trace
  set x() {}
}

We have not any manner to find out where is placed the decorator, over the set or over the get, and as a result there is not mean to apply rightly the decorator.

Is it possible to get a indicator or another flag to kwon where the decorator is placed, whether over the getter or over the setter? The feature would be very useful in some cases.

I can see that the new behavior would be a regression for you. Can you give a little more context about why this situation comes up?

I can see that the new behavior would be a regression for you.

Into new README.md we can read:

@wrap may be used on getters or setters, and applies to these individually.

As a result, our problem is resolved with the the @wrap decorator.

class C {
  @wrap(f) set prop(v) { }
}

is roughly equivalent to the following:

class C {
  set prop(v) { }
}
const descr = Object.getOwnPropertyDescriptor(C.prototype, 'prop');
descr.set = f(C.prototype.method);
Object.defineProperty(C.prototype, 'prop', descr);

But, @register is different and

class C {
  @register(f) set prop(v) { }
}

is roughly equivalent to

class C {
  set prop(v) { }
}
f(C, "prop");

In this case, we don't kwon if the decorator intentionally write over the setter or over the getter.

If we want to decorate the getter / setter pair with @wrap, then two calls will occur, one for each method, but if we do the same with @register the result is a bit strange.

class C {
  @register(f) set prop(v) { }
  @register(f) get prop() { }
}

is roughly equivalent to

class C {
  set prop(v) { }
  get prop() {}
}
f(C, "prop");
f(C, "prop");

Can you give a little more context about why this situation comes up?

Some generic decorators can be applied with full meaning to the getter and/or setter methods. It is the programmer who decides whether to apply this decorator on both or on only one of them. For example: @log, @trace, @performance, @catch, etc.

Other generic decorators only make sense about the getter method or about the setter method. @validate or @throttle applies to the setter method, but does not make any sense about the getter.

Finally, other generic decoratos has sense about the property and need the pair getter/setter. For example, @readonly only about a setter method or only about a getter method does not make sense (we can not mark a getter or a setter as read-only, we do not have a specific descriptor for each of these methods that we can configure as' writable: false '). This decorator only makes sense if it deletes or locks the call to the setter and keeps access to the getter.

Therefore, we have all the possible cases and combinations and they all make sense.

It seems reasonable that any decorator applied to a getter or a setter both has access to the coalesced pair, and knows on which of the two it is applied.

That is definitely a possible design, but coalescing was a rather complicated part of the specification, and the use cases were unclear. In the January 2019 TC39 meeting, there was a general concern expressed that the decorators proposal was too big and complicated, so eliminating this aspect was an attempt to respond to that feedback. Note that a coalesced getter/setter in a class is just a concept that doesn't exist before this proposal. This issue is useful to understand some cases; any more times when you all can think of this coming up would be useful.

Note that a coalesced getter/setter in a class is just a concept that doesn't exist before this proposal.

If that's the case, then why make it so in this proposal? It's fairly clear that applying a decorator to the handlers of a property separately has merit.

I'd be interested to hear how the new decorators proposal works for this aspect of these use cases.

In particular, the new proposal lets the decorator know, in the context object, whether it was placed over a getter or setter. I think that should resolve this issue, right?

For the most part, if I'm understanding the current README.md, this should work. There are some curiosities though. In the comments about @set not being possible, there is this:

  • If a setter is inherited, it is possible to write a decorator for a field which specifically calls super getters and setters, rather than using the underlying storage.

I'm a little perplexed by this. If the original class definition looks like so:

class Base {
   #a = "some constant"; 
   get a() { return this.#a; }
   set a(v) { this.#a = v; }
}

class Ex extends Base {
   @inherited a = 2;
}

According to the new proposal, Ex::a gets wrapped. If Ex::a were a function, I'd understand this. However, if the purpose of @inherited is to ensure that Ex::a is bound to Base::a, then it would mean replacing Ex::a with an accessor. However, that also means that there is no longer any "underlying storage" that exists in Ex save for what was inherited from Base. This is not a wrapping but a replacement. So how do you go about writing such a function and ensure that the initialization value is passed along properly given that you can't have both value and get/set in a property declaration? Remember, the expected intention is that the initialization value is passed to the Base's accessor methods.