ember.js icon indicating copy to clipboard operation
ember.js copied to clipboard

Tracked properties are non-enumerable

Open ef4 opened this issue 6 years ago • 13 comments

Class properties are enumerable:

class Foo {
  someProp;
}
console.log(Object.keys(new Foo()));
// prints ["someProp"]

But if we mark one as @tracked, it becomes non-enumerable:

class Foo {
  @tracked
  someProp;
}
console.log(Object.keys(new Foo()));
// prints []

This seems bad and confusing so I suspect it is unintentional.

ef4 avatar Jul 26 '19 05:07 ef4

I agree this is counterintuitive and not ideal, but unfortunately its a result of adding a native getter/setter that intercepts calls to tracked properties. Those getters/setters are on the prototype of the object instead of the instance, and they should be enumerable there.

I don't think this is something we can solve at the moment unfortunately, since tracked properties rely on having a way to intercept calls 😕 definitely open to suggestions, if we do figure out a way that would be great.

pzuraq avatar Jul 26 '19 18:07 pzuraq

Digging in, I realize this also means we change the timing of class property initialization. (I realize maybe I am late to the party and I vaguely recall people talking about this before.)

let counter = 0;
class X {
  // if you removed tracked here, you get a different answer
  @tracked
  thing = (function() { return counter++ })();
}
let x = new X();
counter++;
console.log(x.thing);

Both of these are tied to the same problem. Class properties are sugar for doing work in the constructor. Not on the prototype.

Why do we need to intercept on the prototype? It's definitely possible on the instance, and would fix both problems:

let internal = new WeakMap();
class Foo {
  constructor() {
    internal.set(this, 'initial value');
    Object.defineProperty(this, 'someProp', {
      enumerable: true,
      get() {
        console.log('intercepted get');
        return internal.get(this);
      },
      set(value) {
        console.log('intercepted set');
        internal.set(this, value);
      }
    });
  }
}
let f = new Foo();
console.log(f.someProp);
f.someProp = 'new value';
console.log(f.someProp);
console.log(Object.keys(f));

Are we afraid it's slower?

ef4 avatar Jul 26 '19 19:07 ef4

Yes, we’re definitely afraid it’s slower. The initialization problem will be solved with future iterations on the spec: https://github.com/tc39/proposal-decorators/blob/master/README.md#tracked

I’d be hesitant to lock ourselves into any functionality that could be a perf bottleneck, such as enumerability. I’d actually very much like to fix the initialization issue since that’d actually be more performant, most likely. We could try benchmarking the difference to see if it would be problematic, but I would also be worried that we would be diverging from the way that decorators are meant to be used if we were to attempt to assign getters/setters to every instance.

pzuraq avatar Jul 26 '19 20:07 pzuraq

Are we afraid it's slower?

Yes, absolutely.

rwjblue avatar Jul 30 '19 16:07 rwjblue

I see. Ok.

Is there any manual workaround that allows an app author to make a property that's enumerable while also being tracked?

ef4 avatar Aug 05 '19 16:08 ef4

Not currently, but I think that would be something that's reasonable to add. Either something like:

@tracked({ enumerable: true });

or

@enumerableTracked

This would also be possible to experiment with as soon as we get the public API for autotracking nailed down, which I think we're getting closer to

pzuraq avatar Aug 05 '19 23:08 pzuraq

Is this issue still relevant? I read through the conversation but I'm not sure if there's work on the table.

locks avatar Apr 19 '20 11:04 locks

Yes it’s still relevant. I think it’s better if people who hit this problem can find an open issue about it, rather than opening new issues.

ef4 avatar Apr 19 '20 14:04 ef4

I actually don't think this would actually be possible with the current behavior of decorators. We cannot install accessors on the instance with them, because the only way to do that is via class field initializers, and that would get overwritten by the class field being assigned.

So I believe this issue is in "won't fix" territory. The only possibility is if the new decorators spec does support this somehow 🤞

In the meantime, if users do need enumerability and tracking on the same object, I think a solution like TrackedObject would work in general: https://github.com/pzuraq/tracked-built-ins

Since it uses Proxy instead of decorators to autotrack, it can enumerate correctly.

pzuraq avatar Apr 20 '20 21:04 pzuraq

I think this is the right place to post this... I just came across this and was disappointed to see that setProperties or Object.assign won't work:

https://ember-twiddle.com/e73d9229d22fafae86dcae058e6b6737?openFiles=routes.application%5C.js%2C

amk221 avatar Jul 10 '20 14:07 amk221

Yes, unfortunately this is still wont-fix. Decorators as they currently exist cannot work this way performantly. It is also unlikely that they will be able to work like this in the final spec, based on the current direction of the spec and the constraints that have been given to us by engines. This would be an issue to raise over there though, if you wanted to: https://github.com/tc39/proposal-decorators

pzuraq avatar Jul 13 '20 17:07 pzuraq

Coming to this almost 4 years later… would this be solved by something like the new class auto-accessors part of the proposal? Probably not since they are also on the prototype and not on the instance, but just thought I’d ask. As a matter of fact I haven’t read anywhere that class auto-accessors would be enumerable anyway.

BTW, @pzuraq, the link you provided is broken since the proposal has since been updated, and the @tracked section has been removed. Here’s the permalink: https://github.com/tc39/proposal-decorators/blob/ff606867a6355725e41617a0063ca74ca773b089/README.md#tracked

chharvey avatar May 02 '24 19:05 chharvey

would this be solved by something like the new class auto-accessors part of the proposal?

I don't think so -- see this example from signal-utils: image

tho, maybe there are more properties to define on the decorator right now I just have get / set / init

NullVoxPopuli avatar May 02 '24 21:05 NullVoxPopuli