Allow arrow functions getters

When defining a getter on an object, only the shorthand syntax is currently allowed. I propose we also allow defining getters through assignment as follows:

{
  // Already allowed
  getX: () => this.#x,

  // Now this is allowed too
  get x: () => this.#x,
}

class MyClass {
  // Already allowed
  getX = () => this.#x

  // Now this is allowed too
  get x = () => this.#x
}

It shouldn't really add any more complexity to javascript, as we're just reusing semantics that are already familiar to end-users. I view it as simply lifting a restriction that doesn't really need to be there.

It also helps to make writing getters a little less verbose. Compare these two classes:

class User {
  // ...
  get name() {
    return this.#name
  }
  get birthday() {
    return this.#birthday
  }
  get age() {
    return this.#age
  }
}

// vs

class User {
  // ...
  get name = () => this.#name
  get birthday = () => this.#birthday
  get age = () => this.#age
}

Thoughts?

The difference from object literals is that method syntax in a class goes on the prototype, arrow functions assignment are created on the instance.

So arrow functions getters are not just a shorter syntax than the existing getter syntax.

Is there a particular scenario where installing the getter on the class instance is desired?

Correct - but in most cases, this won't really make much of a difference. just like how "=>" is technically not interchangeable from "function() {}", but in most cases, it's not a big deal which one you use.

I'm going to flip this around. Is there a particular scenario where installing the getter on the class instance would be undesirable? (laying aside micro performance differences). In most scenarios, it should be interchangeable whether or not the method is on the prototype or the instance, and when there's actually a need to put it on the prototype, the long-form can be used instead.

It would overwrite instead of override.

class A {
  get x: () => 1
}
class B extends A {
  get x: () => super.x + 1 // undefined + 1 (B's x overwrites A's x)
}

new B().x // NaN

or in the case of being in the base class only, there would be an override but installed by the base class

class A {
  get x: () => 1
}
class B extends A {
  get x() { return 2 }
}

new B().x // 1 (A's instance version overrides version in B's prototype)
1 Like

The whole point of a getter usually is to provide different behavior based on the receiver. The whole point of an arrow function is that it doesn't have different behavior based on the receiver. Can you help me understand your use cases where you need an arrow function as a getter?

@senocular

Ah, yes, those are interesting use cases I didn't think about. But, none of them create surprising behaviors, and they only apply in scenarios when you're designing your class to be able to be easy to inherit. So, if you're designing a base class, you would just want to make sure you're using the long form of getters, just like you would want to make sure you use class { yourFunction() {} } syntax over class { yourFunction = () => {} } syntax, even though the latter gives you nice auto-this-binding characteristics. If you fall into the majority of other scenarios where you don't intend for your class to be inherited, then you can use these kinds of shorthands freely.

@ljharb - The this-binding properties of an arrow function are not a need here. In fact, I can't think of any reason why an arrow function would be any better over a normal function functionality-wise when used as a getter. This proposal is intended primarily for convenience, not for functionality.

right, but convenience for what? if there's no use case for an arrow function as a getter, then why would it make sense to add special syntax for it?

It just makes code a little bit shorter when you're creating a bunch of getters in a single class.

class MyClass {
  get username() {
    return this.#rawData.#username
  }
  get dateOfBirth() {
    return this.#rawData.dateOfBirth
  }
  // ... many more getters
}

// vs

class MyClass {
  get username = () => this.#rawData.#username
  get dateOfBirth = () => this.#rawData.dateOfBirth
  // ... many more getters
}

This certainly isn't a ground-breaking improvement, just a little nice-to-have. Normally I wouldn't propose ideas that are primarily for "saving a few characters" purposes, but this particular one felt more like lifting a restriction rather than adding new syntax, so I went with it.

It's not just lifting a restriction, though, because arrows capture their lexical this, whereas in this proposal this would be set at the call site as happens with shorthand methods. So the syntax you're proposing isn't actually arrows, it's arrows-with-different-this-semantics. I think that would be very confusing.

2 Likes

I'm not sure what you're meaning - I'm not proposing that we do any changes to the semantics of the arrow function. Technically, under this proposal, things like this would be possible too:

class MyClass {
  get x = functionFactory()
  get y = function yGetter() { ... }
}

The "this" binding for arrow functions shouldn't be any different from how "this" gets bound in this scenario:

class MyClass {
  f = () => this.x
  x = 2
}

const o = new MyClass()
o.f() // 2

From the OP:

Existing code,

({
  x: () => this.#x,
})

In that snippet, this refers to whatever it does outside of the object literal, not to the object itself. If that changes when you change x to get x, you've changed what this means in that arrow. If not, it's going to be confusing for other reasons - many readers will reasonably expect that it refers to the object. It'll be confusing either way.

I probably threw together that code snippet too quickly. Normally when using object literals, I wouldn't recommend using "this" at all - a better use case for object literals would be something like this:

const createUser = ({ name, birthday }) => ({
  name,
  birthday,
  get age: () => calcAgeFromBirthday(birthday),
  // ...
})

This is correct

This behavior does not change when you use get x - it'll still make this refer to whatever is outside the object literal. You're right that this will be confusing to readers - just as confusing as using this in a normall arrow function in a object literal. This syntax doesn't add any new confusing behavior that the language doesn't already have - it's consistent with the existing weirdness of the language.

x: is a form which creates an arbitrary data property, and the RHS works there exactly like it does elsewhere; there's no weirdness in that.

But get x as proposed here creates specifically a getter, which is not an arbitrary data property. I think a lot of people would quite reasonably expect that this in a syntactic form which is specifically for creating a getter would refer to the object on which it is installed.

I'm not sure why anyone who's familiar with how "this" works would expect behavior any different than what I explained ...

Let's disect this chunk of code:

class C {
  f() {
    const data = {
      get f: () => this
    }
  }
}

First we have f(). When f() gets called a "this" value gets implicitly passed in, most likely it'll be an instance of C. During the invocation of f() there's no reason to expect that the value of "this" will suddently be anything different within any sub-expression. The only time it should change is if you're defining another function, which may also have "this" get implicitly passed in that shadows the outer this.

Next we have get f: <expression>. That is within the body of f(), so it's this should be bound to what was implicitly passed in to f() - no reason to expect anything different here.

Inside of this expression we're creating an arrow function. The arrow function captures the value of this from its surrounding scope, so whenever the arrow function executes, the "this" value will be whatever it was when f() was invoked.

Note that nothing in those above steps had anything to do with the fact that we were doing a get x - we could have been doing any of the following things instead, and it would have worked exactly the same, because all of these arrow functions would have been executed in the context of f():

f() {
  const data = {
    a: () => this // "this" got captures from f()
  }
  const b = () => this // "this" got captures from f()
  2 + (() => this)() // "this" got captured from f()
  // etc
}

Having the value of "this" get shadowed in a particular sub-expression would be odd and unintuitive, and would go against what anyone would have expected, if they've reached the point of finally understanding the oddities of "this". I can't see any reason why someone might expect "this" to behave any differently inside "get x: () => this" over "x: () => this" - if so, would they also expect "this" to behave differently inside potential future language features, like, perhaps "readonly x: () => this"?

Even if people don't really understand how "this" binding works, I would argues that there are many cases where an arrow function would provide a more intuitive result than a normal function. For example:

class User {
  ...
  #name
  getInfo() {
    return {
      get name() {
        return this.#name // Error! this.#name does not exist
      },
      get name2 = () => this.#name // Works, as one could resonably expect
    }
  }
}

Umm... and I guess that example also showcases a useful functionality difference that this proposal provides - how else would you use the correct "this" binding in that example? By renaming the outer this to self?

I'm assuming that you aren't proposing to make get x = 0 legal, so get x = () => whatever must be a special form where the RHS allows specifically a function expression or arrow expression. (If you are proposing to make get x = 0 legal, I think that's a bad idea.) And I think it's pretty reasonable for a reader to assume that this, in a syntactic form which is specifically for defining a getter, would refer to the object with which the getter is associated. I don't think this is reconcilable.

get x = 0 would be a runtime error, not a syntax error. Just like Object.defineProperty(obj, 'x', { get: 2 }); is a runtime error.

I'm proposing all of these could be allowed:

const data = {
  get a = function f() { return 1 }
  get b = () => 1
  get c = factoryFunction({ someConfig: true })
  get d = someExistingFunction
  // etc
}

Doing things like get d = someExistingFunction could only be allowed if we allowed any dynamic expression to be used there, including some potentially invalid ones, like get x = 0.

Ah, ok. I didn't understand that this was the proposal.

My position is that it is not worth adding a syntactic form for defining getters with arbitrary values, particularly if it allows nonsensical code like get x = 0. The benefit seems small, and the cost - it's new syntax with sharp edges - seems quite high.

1 Like

I don't think this syntax is any more "sharp" than plenty of our existing syntax. It's common to have syntax that only operates on certain types:

> 2()
Uncaught TypeError: 2 is not a function
> null.x
Uncaught TypeError: Cannot read property 'x' of null
> for (const x of { y: 2 }) { }
Uncaught TypeError: {(intermediate value)} is not iterable
// etc...

Sure get x = 0 is nonsense, but so is 2(), and I doubt either of them are a very big source of bugs, precisely because they're nonsense - it's not something people generally try to do, and if they do, they'll get a nice error message telling them exactly what they're doing wrong.

With that said, I can respect your position - I can understand a dislike for having extra restrictions placed on get x: <expression> that don't exist on x: <expression>.

Yes, that's exactly how I did that here for example.

A getter that needs lexical this is an oddball.

The example I linked obviously uses defineProperty, not your proposed syntax, but it did exactly the same thing β€” define a getter with lexical this binding. When I had to replace arrows with old-style functions for compatibility reasons, it forced me to capture the outer this in a variable. I'd say it made the code easier to understand, as it leaves no room for confusion about "which this is this?"

how about:

{
    get X(): this.Y
    /* same as
         get X: function() {
             return this.Y
         }
    */
}

and

class {
    get X() = this.Y
    // i don't like this oneπŸ˜…
}