Due to the fact that the myMember
property is accessed in the parent constructor (the init()
function is called during the super()
call), it becomes impossible to define it in the child constructor without encountering a race condition.
Fortunately, there exist several alternative strategies to tackle this issue.
init
Hook
The init
method can be treated as a hook that should not be invoked within the class constructor. Instead, it can be explicitly called:
new B();
B.init();
Alternatively, frameworks may implicitly invoke it as a part of the application lifecycle.
Static Property
If the property is meant to be a constant, consider using a static property.
This approach is highly efficient as static members serve this purpose. However, the syntax may appear less elegant since referencing a static property in child classes requires the use of this.constructor
instead of the class name:
class B extends A {
static readonly myMember = { value: 1 };
init() {
console.log((this.constructor as typeof B).myMember.value);
}
}
Property Getter/Setter
A property descriptor with get
/set
syntax can be defined on the class prototype. For primitive constants, a getter suffices:
class B extends A {
get myMember() {
return 1;
}
init() {
console.log(this.myMember);
}
}
If the property is non-constant or non-primitive, employing getters and setters may seem more convoluted:
class B extends A {
private _myMember?: { value: number };
get myMember() {
if (!('_myMember' in this)) {
this._myMember = { value: 1 };
}
return this._myMember!;
}
set myMember(v) {
this._myMember = v;
}
init() {
console.log(this.myMember.value);
}
}
In-Place Initialization
An intuitive solution involves initializing the property where it is first accessed. If this occurs within the init
method where this
can be accessed before the B
class constructor, initialization should take place there:
class B extends A {
private myMember?: { value: number };
init() {
this.myMember = { value: 1 };
console.log(this.myMember.value);
}
}
Asynchronous Initialization
If the init
method transitions to an asynchronous operation, the class must implement an API to manage initialization state, for instance, through promises:
class A {
initialization = Promise.resolve();
constructor(){
this.init();
}
init(){}
}
class B extends A {
private myMember = {value:1};
init(){
this.initialization = this.initialization.then(() => {
console.log(this.myMember.value);
});
}
}
const x = new B();
x.initialization.then(() => {
// class is initialized
})
While this approach may not be ideal for synchronous initializations like in this case, it proves beneficial for managing asynchronous routines.
Desugared Class
To work around limitations involving this
prior to super
in ES6 classes, converting the child class to a function may provide a solution:
interface B extends A {}
interface BPrivate extends B {
myMember: { value: number };
}
interface BStatic extends A {
new(): B;
}
const B = <BStatic><Function>function B(this: BPrivate) {
this.myMember = { value: 1 };
return A.call(this);
}
B.prototype.init = function () {
console.log(this.myMember.value);
}
This method is rarely recommended as additional typing is required in TypeScript. Moreover, it does not support native parent classes (TypeScript es6
and esnext
target).