lit-element: Decorators currently don't work with Babel

May be related to #205.

Essentially, given:

@property({ type: String })
prop = 'foo';

Babel configured with:

It seems that one of the two results in this babel helper overwriting our property descriptor, so we lose the getter/setter and lit no longer is aware of changes.

I don’t think it is a lit bug but it is something we need to track. I’m not sure which of the two babel plugins causes this as it needs a little more investigation (it looks to be used in the decorators plugin but specifically for class properties).

edit:

after further investigation it does look like #205, because it will return decorator(...) || descriptor. meaning as long as we return, it will be fixed.

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Reactions: 5
  • Comments: 28 (7 by maintainers)

Most upvoted comments

Here’s the solution (after wasting hours on this):

In your babel config, set decoratorsBeforeExport: true

plugins: [
  ["@babel/plugin-proposal-decorators", { decoratorsBeforeExport: true }],
  ["@babel/plugin-proposal-class-properties", { "loose": true }]
]

Or just calling this.requestUpdate(). But again, that entirely beats the purpose of using the decorator

There’s two issues here with babel’s output in reality. #206 doesn’t seem to fix it…

  • Babel overwrites the descriptor we set on the prototype but allows us to overwrite it if our decorator returns a descriptor
  • Babel then overwrites the descriptor again in the constructor to initialise the class property, this time without caring what was there before

Are you asking how to make your own that support both or do you think lit’s don’t work?

I was asking how to do it in general, but now I also inspected lit-element’s:

After doing some research these past few days, it turns out there’s actually not just two, but six configurations that decorator authors should ensure their decorators work in:

  1. TypeScript experimentalDecorators with useDefineForClassFields set to false
  2. TypeScript experimentalDecorators with useDefineForClassFields set to true
  3. Babel with proposal-class-properties loose set to true and proposal-decorators legacy set to true
  4. Babel with proposal-class-properties loose set to true and proposal-decorators legacy set to false (since Babel 7.1)
  5. Babel with proposal-class-properties loose set to false and proposal-decorators legacy set to true
  6. Babel with proposal-class-properties loose set to false and proposal-decorators legacy set to false (since Babel 7.1)

I started writing a list of differences between Babel and TypeScript legacy decorators at https://github.com/babel/babel/issues/8864#issuecomment-688535867. If you know any more details to add, please do comment there.

I do see lit-element’s decorators support both legacy and newer decorators (“newer” because there’s even newer decorators that no tools support yet and plus the proposal has been put on pause in order to come up with a fourth form of decorators), but I haven’t specifically tested it in all 6 of the above scenarios.

From a quick glance at

https://github.com/Polymer/lit-element/blob/2de92b532b8adad540b91f208063b2e15777029f/src/lib/updating-element.ts#L313-L334

it seems that lit-element decorators may fail in scenario 2, and possibly also in 5 and 6 because I think loose: false is basically equivalent to TypeScript’s useDefineForClassFields: true, so the decorator accessors get overridden by new property descriptors on this (a.k.a. [[Define]] semantics).

Did I overlook if lit-element has an alternative for that case?

Lit-element’s decorators don’t return anything, so they won’t run into the problem of [[Define]] semantics being used on class fields when legacy decorators return descriptors regardless of loose being set to true (tracked in https://github.com/babel/babel/issues/12040).

I’m not sure if Babel is correct (always use [[Define]] semantics if legacy decorators return a descriptor, regardless of the class-properties loose option) or if TypeScript is correct (never use [[Define]] semantics even if a legacy decorator returns a descriptor, when useDefineForClassFields is false).

The problem with supporting both TypeScript and Babel 7 legacy decorators is that they have a number of differences.

What’s the solution for writing a legacy decorator that work in both TypeScript and Babel?

EDIT: I removed some details from this comment (see my next comment for a link to a list of difference between Babel and TypeScript decorators)

It is definitely correct to return a descriptor, no question there. This is how it’ll be going forward.

But there is still a slight issue in babel’s class properties plugin as you can see here.

This helper gets called in the constructor of our class. It’ll modify the existing descriptor (which is why your fix works) but causes some strangeness because of it adding a value.

Anyhow i will review your PR as there’s a few comments.