Allow arrow functions getters

Well, part of my hope in this was to not add new syntax conceptually to the language (Yes, it's technically new syntax, but it can also be viewed as simply lifting a syntactic restriction). I wouldn't want to create any novel syntax for something as unimportant as the ability to create getters in one line.

If it's only about creating a getter in one line, I'm just not seeing the value. I tried hard, but in the end:

{
   get x() { return this.y },
   // vs
   get x: () => this.y
}

where the former does what we expect, and the latter does something odd by grabbing this from the outer context, all to save ~4-6 keystrokes just doesn't seem worth it.

You're right that it's a very, very minor improvement, but I thought it was a pretty straight forwards and intuitive change - I didn't realize people would find the way arrow functions bind "this" when placed in an object literal so confusing, to the point of discouraging any syntax that encourages people to put arrow functions in object literals, but apparently that's how it is :man_shrugging:.

It's not just that. It's that you're wanting to open this route purely for the sake of making one-line functions more compact, but the net result is actually also dragging in a this binding from the outside context, and (in the case of a class member) dragging in the "fields" problem of putting a definition on the instance. Not everyone sees the last one as an issue, but it is for some of us. In either case, that's too many side effects.

Here's an alternative, but once again, you lose the "simply removing a restriction" angle.

{
   get x() => this.y,
   // Equivalent to
   get x() { return this.y }
}

With this approach, you get the short notation for a 1-liner, but none of the side-effects of using arrow notation. It almost looks like an arrow function, but arrow functions are always anonymous. What's more is that this notation lends itself to class member, and can even lend itself to normal function declarations.

class Ex {
   get x() => this.y
}

function fn() => someExpression;

In this way, you get shorthand benefits wherever 1-line functions exist, with no real downside. I'm not saying I like this kind of shorthand at all, but at least this doesn't seem to have any real issues.

1 Like

I recently found myself wanting to install a getter using short syntax on a plain object. This is particularly useful for cases where a closure captures private state.

function makeFoo() {
  let bar = 42;

  const getBar = () => bar;

  return {
    get bar: getBar,
  };
}

Sure in this simplified example, an inline get bar() { return bar; } would have worked just as well, but in my case the getter and updater are returned by another factory function.

It basically would allow the pattern of declaring the property getter first then installing it on the object using syntax, like can be done for regular properties.

const foo = () => 42;
const obj = {foo};

The alternative today is to explicitly call defineProperty which is cumbersome. What I ended up doing is to transform my internal factory function to return a plain object with the getter explicitly defined on it, and use Object.assign() to copy it onto the target. That however is a pain to type properly.

As @theScottyJam describe, this would strictly have the same semantics as own property definition, but acting on the get (and set) of the descriptor instead of value. And while it does allow arrow-functions to be used, I only see that as a side-effect of the define semantics.

While I have yet to find a use case for instance field accessors defined the equivalent way (get prop = getProp;), one could argue to allow them as well for symmetry.

1 Like

If it helps anyone, here is my utility function I wrote to help with this:

/**
 * @template {{ [key: string]: () => any }} T
 * @param {T} getters
 * @returns {{
 *   readonly [P in keyof T]: ReturnType<T[P]>;
 * }}
 */
export const makeGetters = (getters) =>
  Object.create(
    null,
    Object.fromEntries(
      Object.entries(getters).map(([key, get]) => [
        key,
        { get, enumerable: true, configurable: true },
      ]),
    ),
  );

It can be used like this:

function makeFoo() {
  let bar = 42;

  const getBar = () => bar;

  return Object.assign({
    baz: 'baz',
  }, makeGetters({
    bar: getBar,
  }));
}

If you're using assign(), you're going to lose the getter and instead have a data property. Was that the intention? Or did you mean defineProperties() instead of assign()?

Right! Yeah I forgot I used an assign equivalent which copies property descriptors instead of using [[Get]] and [[Set]]

Is another alternative:

get bar() { return getBar(); }

Or is it important to not introduce a layer of indirection?

Of course everything can be solved with another level of indirection. I would say the verbosity cost now is getting high for no particular reason.

I too was actually just running into a scenario where it was extra difficult to use the current getter syntax, because I needed the this-binding semantics of arrow functions.

I was trying to make a pseudo-replica of the browser's cancelation token API. I did so like this:

class CancelToken {
  #aborted = false

  signal = Object.defineProperties({}, {
    aborted: {
      get: () => this.#aborted,
    }
  })

  abort() {
    this.#aborted = true
  }
}

const token = new CancelToken()
token.abort()
console.log(token.signal.aborted)

I was only making this class for demonstration purposes, but the issue presented here is still a real one. It should be easy to see how cumbersome it is to define a getter on that object literal. Ideally, I would want to write it like this:

signal = {
  get aborted = () => this.#aborted
}

A simple get aborted() { return this.#aborted } solution does not work in this scenario, because this getter needs access to the instance's private state.

That's a great example, it really shows the burden brought by the lack of syntax.

Nit, but the syntax for object literals should be get foo: not get foo =:

signal = {
  get aborted: () => this.#aborted
}

And the indirection "solution" is as bad as the defineProperty approach in this case:

class CancelToken {
  #aborted = false

  signal = (() => {
    const getAborted = () => this.#aborted;
    return {
      get aborted() {
        return getAborted();
      }
    };
  })()

  abort() {
    this.#aborted = true
  }
}
1 Like

Slightly less code with a pipeline.

class CancelToken {
  #aborted = false

  signal = this |> {
      get aborted() {
        return %.#aborted;
      }
  };

  abort() {
    this.#aborted = true
  }
}
1 Like

Or less code with neither?

class CancelToken { #aborted = false;

get signal() { return this.#aborted; } abort() { this.#aborted = true; } }

unless there's some reason you'd need an own getter on a class instance, which seems a different request than this thread.

A more full-featured version of this class would probably look like this:

class CancelToken {
  #aborted = false
  #listeners = []

  signal = {
    get aborted: () => this.#aborted;
    addListener(listener) { ... }
    removeListener(listener) { ... }
    ...
  }

  abort() { ... }
  ...
}

@ljharb

The value in having "signal" be an object of its own is so that you can pass the signal around to anyone who needs to listen for the abort signal, without also giving them power to send the abort signal as well. I've actually ran into a multi-layered privacy scenario like this at other times, I've just never needed to use a getter in conjunction with it until now.

@aclaymore

That's an interesting trick with the pipeline. It works great with such a small example, but it'll start becoming less nice as more and more functionality gets put into the signal object. e.g. What if I wanted to use a pipeline operator within one of the methods on the signal object? I could, but then I'm nesting pipelines. Granted, the Object.defineProperties() version I provided is also not-so-nice as the object grows, I wouldn't want to define each method on signal via defineProperties().

This sort of thing is bringing us back to the days when we had to rename "this" to "self" in order to use it within callbacks, only this time, we're renaming it to a hack token instead.

That's not the API though, signal returns an object with a getter not the current aborted value.

class CancelToken {
  #aborted = false;
  get signal() {
    let self = this;
    return {
      get aborted() { return self.#aborted; }
    }
  }
  abort() { this.#aborted = true; } 
}

Great times.... :stuck_out_tongue_winking_eye: You make a good point, I definitely prefer just being able to use arrow-functions and not having to capture this with variables or .bind all over the place.

2 Likes

my bad, i misread the api :-) having an object have an object of its own is a bit atypical tho.

1 Like

And in the web API the inner object is an instance of a separate class so code ends up with

let AbortSignal;

class AbortController {
    static #friend = Symbol();
    static #AbortSignal = class AbortSignal {
        #controller;
        constructor(friendship, controller) {
            if (friendship !== AbortController.#friend) {
                throw new Error();
            }
            this.#controller = controller;
        }

        get aborted() {
            return this.#controller.#aborted;
        }
    }
    static {
        AbortSignal = this.#AbortSignal;
    }

    #aborted = false;
    #signal = new AbortController.#AbortSignal(AbortController.#friend, this);
    get signal() { return this.#signal; }

    abort() {
        this.#aborted = true;
    }
}
1 Like