tc39/proposal-decorators

how to check if an auto-accessor will throw?

Closed this issue · 10 comments

For a private field #x, I can now use #x in obj to figure out if obj.#x will throw.

For accessor x, however, x in obj won't tell me this; i have to use try/catch to determine it - and when the field is decorated, that approach would invoke decorator code.

How can i determine if the implicit private field in an auto-accessor property is present on an object?

IIUC the problem is to check if the implicit private field is present on an object but not checking if their accessor will throw? All functions can throw anyway.

Yes, but an undecorated accessor will only throw if the receiver lacks the implied private field.

Yeah, that is true on undecorated accessors. But what I mean is that the goal of checking if a function will throw sounds not quite valid to me. And it seems not observable to JavaScript if the auto-accessor is been decorated or not. Yet checking private field existence sounds more doable (compared to checking if a function will throw).

The only person that could check this (if it's syntactic) is the author of the class, who chose whichever decorators are on a class, so someone (like me) wishing to do this check would indeed know this.

So, thinking on this a bit more, I'm unsure if there's a way to change the implementation such that this is no longer an issue at all. Using a private slot ensures that the auto-accessor does not create a value which is observable on the class (e.g. a public prop with a string or symbol key). Maybe we could associate the value more like a WeakMap, but that would be complicated and I think would possibly be even more confusing, since it would still fail use cases like using Proxy with the object but would not fail others.

However, I'm also not sure how common of an issue this will be in practice. It basically requires users to do something fairly unidiomatic. Consider the following:

class Foo {
  accessor x = 123;

  static getX(maybeFoo) {
    return maybeFoo.x;
  }
}

Because x is just a public field name, this is always safe. If maybeFoo is actually an instance of Foo, then it will run the accessor and return the value from the private slot. If it is not a Foo, then it will not attempt to access the private slot, it will just access the value of the x property on the object. The same goes for private accessors:

class Foo {
  accessor #x = 123;

  static getX(maybeFoo) {
    if (#x in maybeFoo) {
      return maybeFoo.#x;
    }
  }
}

Now, #x will be backed by a separate private slot, but we can brand check using #x in on the object, preventing us from ever getting into the situation in the first place.

In order for the user to trigger a failure, it would only be on public fields, and they would have to do something like the following:

class Foo {
  accessor x = 123;
}

const xGetter = Object.getOwnPropertyDescriptor(Foo.prototype, 'x').get;

function getX(maybeFoo) {
  return xGetter.call(maybeFoo);
}

They may also be able to get into this type of situation by getting the property descriptor and then defining it on a separate object. This seems really unidiomatic, and if we think about the equivalent situation, where a user is trying to do the same thing with an arbitrary getter today, it would behave the same way:

class Foo {
  #x = 123;

  get x() {
    return this.#x;
  }
}

const xGetter = Object.getOwnPropertyDescriptor(Foo.prototype, 'x').get;

function getX(maybeFoo) {
  return xGetter.call(maybeFoo);
}

This would fail, and there wouldn't be a way to tell if it would fail without try/catch. Maybe it's somewhat more obvious why it would fail, but otherwise it's pretty similar. Users who aren't defining the class can't arbitrarily use getters either way, and users who are defining the class are unlikely to follow this pattern. So maybe this is a non-issue?

I think that if the spec and desugaring of auto-accessors is not a private field as backing storage, then this is a non-issue.

By using that desugaring in particular in examples and docs, I think it will be confusing for users who think that the backing private field exists, but that they can't access it.

Maybe the answer is not to teach it that way then? While I can understand why it would be a bit confusing if we teach the desugaring, I'm also struggling to see how just having the spec use the same mechanism of private slots would cause issues given that users would have to go out of there way to encounter a problem, and otherwise cannot really observe the existence of the private slot.

Put another way, do you think the case above where a user does Object.getOwnPropertyDescriptor and then misuses the descriptor is actually the issue? Or is it moreso the mental model itself?

If the spec uses it, implementations will surely too, and the error message will match, exposing that its backed by a private field.

It’s a combination - in other words, borrowing a getter is a thing i do all the time. On most userland classes, this is fine. On builtins, it throws when the receiver lacks the internal slots. If I’d like to use an auto-accessor but also avoid throwing, it seems like I’d currently be forced to use a decorator, make another private field in it (effectively; it’d have to be a separate weakmap or something), and then check for the receiver’s presence in the weakmap. That’s a lot more boilerplate than having some kind of mechanism directly inside the class body.

Overall, i agree there’s not much to be done here - I think it’s useful tho to talk out use cases and mental models in case there is something that we can do.

I think this discussion has been tapped, and we won't be making changes to the spec regarding it. As noted, it is a fairly uncommon situation that this error will occur in, and there's not much we can do to prevent users from encountering it. It is also analogous to how getters + private fields work in general, so it's not a new issue. As such, I'm going to close this.

hax commented

Just find this issue, actually it's another proof that why class.hasInstance(o) (test whether the object is the instance of current class, instead of test private field existence) is the better solution than #x in o in most use cases.

When people do "x" in o test, it's a duck type check, which is just "good enough". Eventually it's no guarantee that o.x is really conform to the interface and semantic people want, it could just happen to have the name "x".

On the other side #x in o is testing a very specific part of implementation details which can't be exposed outside, and have very strong guarantee what it is. Note, the goal of check is not avoid throwing (any method could throw for any reason), but get the guarantee that o have specific implementation details.

So we provide two similar syntax for very different things, and accessor make such conflict much obvious. When using accessor, the implementation details are hidden, it would even not imply private fields in the concept model if the keyword was prop.

We could provide different level of guarantees in the language, from the most loose (duck type check) to most strict. Maybe none could satisfy everyone and every case, but as my experience, the most wanted guarantees are class.hasInstance(o) and o implements someProtocol (by first-class protocol proposal), which match o instanceof X or o is X in most other OO programming language.