I use classes all day every day. As much as I want to love the syntax, sometimes I hate it. The [[Define]] semantics of class fields often leads to inevitably ugly code like the following:
class Foo extens Base {
/** @override */
foo = 123
}
// Ugh, put it on the prototype too, because the Base class might read it during construction.
Foo.prototype.foo = 123
Class fields with [[Define]] semantics are a mess.
"Derived class constructors run after base class constructors" was the behavior prior to class fields as well (and is also true in other languages, for that matter). It's a pain point in other languages as well, but there's not much to be done about it; those two things have to happen in some defined order, and it's usually going to be incoherent to run the derived class constructor first.
The pattern I would probably use is, if the base class is OK with part of it's state being set by the subclass during initialization, then that should be part of it's (potentially optional) constructor arguments - so it is explicitly part of the relationship.
class Foo extends Base {
constructor() {
super({ foo: 123 });
}
}
If we were working with the more powerful 1.0 decorators proposal in ES, I'd rather have 2 decorators to handle this:
@usePrototype - class or field-level decorator that initializes all covered fields on the prototype at the time of definition. (Obviously [[Define]] semantics.)
@useSet - class or field-level decorator that ensures all covered fields are initialized using [[Set]] semantics on the instance. (Overrides initialization from @usePrototype.)
Sadly, the new watered down wrapper decorators cannot be used to implement this.
The new "standard decorators" are at stage 3. With the new semantics, as you noticed, it is impossible to do this:
class MyClass {
@usePrototype foo = 123
}
because field decorators do not have access to the class (and hence not class.prototype). It is possible to do it with coordination using a paired class decorator:
@withProtoProps
class MyClass {
@usePrototype foo = 123
}
As for @useSet, that's also possible with a paired decorator.
@withSet
class MyClass {
@useSet foo = 123
}
(a subclass would delete the field and then set it, with one caveat being that this is not ready until the subclass runs). If it is needed in decoratored class's constructor, a helper would be needed:
class MyClass {
@useSet foo = 123
constructor() {
set(this, MyClass) // accesses decorator metadata to determine which fields to set
}
}
Alternatively with no decorators:
class MyClass {
foo = 123
bar = 456
constructor() {
useSet(this, 'foo', 'bar') // prone to human error
}
}
That's simple in plain JS. TypeScript has issues with it, where it takes fields as source of truth for types in .ts files +since before fields existed in JS).
Looks like the class decorator helper would make it worth it