tc39/proposal-decorators

@lazy decorator

Closed this issue · 8 comments

As shown in #342, we can create a decorator that can initialize things lazily within set or get, so ``@lazy` could be implemented in userland.

Maybe it would be convenient to have a built-in @lazy decorator that makes the initializer lazy (on first get, unless set has already been triggered).

Here's problem with Custom Elements and a solution with @lazy:

The following has a runtime error, the subclass can not override the superclass property because this.attachShadow always throws if it is called more than once (and element can only have one ShadowRoot):

class Component extends HTMLElement {
  renderRoot = this.attachShadow({mode: 'open'})

  connectedCallback() {
    console.log(this.renderRoot)
  }

  // ...
}

class MyClass extends Component {
  renderRoot = this.attachShadow({mode: 'closed'}) // runtime error, super class already called this.attachShadow, an unwanted side-effect.
  // ...
}

Solved with @lazy:

class Component extends HTMLElement {
  @lazy renderRoot = this.attachShadow({mode: 'open'})

  connectedCallback() {
    console.log(this.renderRoot)
  }

  // ...
}

class MyClass extends Component {
  // also use @lazy here to allow possible subclasses of MyClass to override renderRoot
  @lazy renderRoot = this.attachShadow({mode: 'closed'}) // no error
  // ...
}

Now it works, because when connectedCallback finally runs at some time after construction, it will read the subclass renderRoot property, at which point the subclass initializer will run, and therefore this.attachShadow will have been called only once using the subclass' version, and the act of "overriding" a super class property was successful.

The unfortunate thing with class fields (and no use of decorators for help) is that they do not "override" superclass fields of the same name.

This proposal does not introduce ways for subclasses to override/change the behavior of superclasses, which this would require if decorating accessors/fields. As such, this is out of scope of this proposal, so I'm closing this issue.

I have another use case in that I want to defer the expensive computation until the field is used for the first time. I found it's impossible in the latest decorator spec.

pzuraq commented

@Jack-Works it is not impossible if you use the accessor keyword

it's impossible whether I use accessors or not

pzuraq commented

@Jack-Works if you use accessor, you can decorate the auto-accessor and add a getter which computes the value the first time it is accessed. Just don’t return an initializer, instead defer the computation via the getter.

@Jack-Works if you use accessor, you can decorate the auto-accessor and add a getter which computes the value the first time it is accessed. Just don’t return an initializer, instead defer the computation via the getter.

how? I don't see anything related in the first or second argument when the decorator function gets called.

to be clear I mean

@lazy accessor x = new HeavyObject()

not

@lazy accessor x = () => new HeavyObject()
pzuraq commented

Ah, understood. Yes that would unfortunately be impossible. The inability to capture initializers during decoration was a hard requirement from engines, who believed doing so would make initialization impossible to optimize.