◆ JavaScript のクラスフィールド機能では setter が呼び出されずプロパティ定義される
◆ LitElement のリアクティブプロパティ機能が動かなくなる
◆ プロパティを消して setter を呼び出す
  ◆ 共通化しようとすると非同期化が必要になって 同期的には実行できない
◆ LitElement の各プロパティの設定オブジェクトにデフォルト値を指定できるようにしたほうがいいかも

クラスフィールドを使えない

久々に LitElement を使ったのですがやっぱり不満なのが クラスフィールドで初期値を書けないところです
コンストラクタに書く必要があります

<script type="module">
import { LitElement, html } from "https://cdn.skypack.dev/lit"

class FooBar extends LitElement {
static properties = {
num: {},
}

constructor() {
super()
this.num = 10
}

up = () => {
this.num++
}

render() {
return html`<button @click=${this.up}>${this.num}</button>`
}
}
customElements.define('foo-bar', FooBar)
</script>
<foo-bar></foo-bar>

これは問題なく動作します
初期値が 10 でボタンを押すたびにカウントアップします

num に初期値 10 を設定する方法をコンストラクタ内での代入からクラスフィールドに変更すると動かなくなります
ボタンを押してもずっと 10 です

<script type="module">
import { LitElement, html } from "https://cdn.skypack.dev/lit"

class FooBar extends LitElement {
static properties = {
num: {},
}

num = 10

up = () => {
this.num++
}

render() {
return html`<button @click=${this.up}>${this.num}</button>`
}
}
customElements.define('foo-bar', FooBar)
</script>
<foo-bar></foo-bar>

コンストラクタはできれば省略したいので 書かないといけなくなるのは不満です

これができない原因は JavaScript の仕様によるものです
クラスフィールドはプロパティへの代入処理ではなく プロパティ定義となっています
継承する親クラスで prototype に setter を作っていても setter を経由する代入ではなく 直接プロパティ定義が行われるので setter が呼び出されません

LitElement が提供する getter/setter を使うことでリアクティブプロパティを実現しているので それが使われないとリアクティブ化できないというわけです

プロパティを消して setter を呼び出せば

しかし仕組み的にいえば プロパティを消して setter を呼び出せば問題なく動きそうです

ということでこれを試してみます

<script type="module">
import { LitElement, html } from "https://cdn.skypack.dev/lit"

class FooBar extends LitElement {
static properties = {
num: {},
}

num = 10

constructor() {
super()
const num = this.num
delete this.num
this.num = num
}

up = () => {
this.num++
}

render() {
return html`<button @click=${this.up}>${this.num}</button>`
}
}
customElements.define('foo-bar', FooBar)
</script>
<foo-bar></foo-bar>

動きました
ボタンでカウントアップできています

それならこの処理を LitElement 側でやってくれればいいのにと思うのですが クラスフィールドによってプロパティが定義されるのは 親のコンストラクタを呼び出した後です
LitElement のコンストラクタの処理中には まだクラスフィールドによる定義が行われていないので この処理を行うことができません

かと言ってあきらめたくないので 非同期処理化してプロパティ削除と setter 呼び出しを行うようにしました
直接 LitElement を書き換えたくないので間にクラスを 1 つ挟みます

<script type="module">
import { LitElement, html } from "https://cdn.skypack.dev/lit"

class CFLitElement extends LitElement {
constructor () {
super()
Promise.resolve().then(() => {
for (const k of Object.keys(this.constructor.properties)) {
if (Object.hasOwn(this, k)) {
const tmp = this[k]
delete this[k]
this[k] = tmp
}
}
})
}
}

class FooBar extends CFLitElement {
static properties = {
num: {},
}

num = 10

up = () => {
this.num++
}

render() {
return html`<button @click=${this.up}>${this.num}</button>`
}
}
customElements.define('foo-bar', FooBar)
</script>
<foo-bar></foo-bar>

要素が作られた直後に非同期処理を挟んで リアクティブプロパティが設定されることになるのが少し気になるところですが 基本的に問題なく動きそうです
場合によっては connectedCallback が同期的に呼び出されることもあり クラスフィールドのリアクティブプロパティ化より先に実行されます
connectedCallback かどっちか先に呼び出されたほうで初期化もできます

class CFLitElement extends LitElement {
constructor() {
super()
Promise.resolve().then(() => {
this._cfInit()
})
}

connectedCallback() {
super.connectedCallback()
this._cfInit()
}

_cfInit() {
if (this._cf_init) return
this._cf_init = true

for (const k of Object.keys(this.constructor.properties)) {
if (Object.hasOwn(this, k)) {
const tmp = this[k]
delete this[k]
this[k] = tmp
}
}
}

_cf_init = false
}

ですが createElement してメソッド呼び出しだと 任意のメソッドが先に実行されることになって対応が難しいですし setter を使ってないだけでプロパティとして値は存在して書き換えも可能です
書き換えられていれば クラスフィールドの処理時に書き換え済みの値をセットしますし その後に更新処理が発生して画面にも反映されます
なのでただの非同期処理化だけでいいかもと思ってます

default オプションを追加する

LitElement のリアクティブプロパティでは properties に type 等の設定を書く場所が用意されています
それならここで初期値を設定できるようにしてほしいですが 現状は用意されていません

ここに書くのはインスタンスごとのものではなくて全インスタンス共通のものだからでしょう
ただそれなら関数形式にして毎回新しい値を作成すればいいんです
ということでこういうものを作ってみました

<script type="module">
import { LitElement, html } from "https://cdn.skypack.dev/lit"

class CFLitElement extends LitElement {
constructor () {
super()
for (const [k, v] of Object.entries(this.constructor.properties)) {
if (typeof v?.default === "function") {
this[k] = v.default()
}
}
}
}

class FooBar extends CFLitElement {
static properties = {
num: {
default: () => 10
},
}

up = () => {
this.num++
}

render() {
return html`<button @click=${this.up}>${this.num}</button>`
}
}
customElements.define('foo-bar', FooBar)
</script>
<foo-bar></foo-bar>

これなら非同期処理にもならない上に プロパティ定義と近い場所でデフォルト値がわかって読みやすいです
それに初期値を設定するだけのコンストラクタも不要です

こうしてくれればいいのにと思うのですが Lit プロジェクト自体は TypeScript で作られていて TypeScript が前提になっています
プロパティ定義の方法は TypeScript ではデコレータを使うようになっていて 結構違っています
TypeScript で書くなら困らないものなので JavaScript のために追加されることはあまり期待できません
自分でこういう感じのラッパークラスを用意するのがいいかもですね