TypeScript brings full class-based inheritance to JavaScript projects. This works very well, but you may run into some unexpected behavior when setting properties in a subclass and using those properties in the parent constructor.
Here is a simple example that demonstrates the problem:
- MyBaseClass is defined as an abstract class. Like other OOP languages, this means that the class cannot be instantiated directly.
- MySubClass is a concrete subclass of MyBaseClass that can be instantiated.
- New in TypeScript 2.0 is the ability to define properties as abstract, which I have done with string1 and string2. These properties must be set in the subclass, or the transpiler will generate an error.
- The parent class constructor sets the string3 property based on the values of string1 and string2 set in the subclass. Imagine that string3 is a property that will be used by other methods in the base class (not shown in the example code), so it is a valid design choice to set that property in the constructor.
- Finally, the last two lines of code instantiate the class and display string3.
// Abstract base class
abstract class MyBaseClass {
// Abstract properties to be set in subclass
protected abstract string1: string;
protected abstract string2: string;
public string3: string;
constructor() {
// Set string3 so it is available for rest of class
this.string3 = this.buildString3();
}
protected buildString3(): string {
return this.string1 + " " + this.string2;
}
}
// Concrete subclass
class MySubClass extends MyBaseClass {
// Abstract properties must be set or
// transpiler error will occur
protected string1 = "Hello";
protected string2 = "World!";
}
// Create object and display string3
var myObject = new MySubClass();
alert(myObject.string3);
Of course, I expected this code to display “Hello World!”, but in fact it displays “undefined undefined”. Why is that? A look at the transpiled Javascript of the subclass constructor will give us a clue.
function MySubClass() {
_super.apply(this, arguments);
// Abstract properties must be set or
// transpiler error will occur
this.string1 = "Hello";
this.string2 = "World!";
}
As you can see, the subclass properties aren’t set until AFTER the base constructor is called. Ryan Cavanaugh from Microsoft explains:
This is the intended behavior.
The order of initialization is:
1. The base class initialized properties are initialized
2. The base class constructor runs
3. The derived class initialized properties are initialized
4. The derived class constructor runs
Follow the link for more details on the reasons, but it comes down to the fact that property initialization is inextricably intertwined with the constructor. Alternative approaches have been suggested, but besides breaking existing code, the order above is likely to become part of the EcmaScript (JavaScript) standard.
As an OOP veteran of other languages, I find this behavior unfortunate. By defining a class as abstract, you are in effect saying it is “incomplete“, and it will be completed by its concrete subclasses. These technical restrictions on property initialization and constructors get in the way, but there are things we can do to work around the problem.
Constructor Parameters
Rather than setting properties in the subclass, you can pass values to the base class constructor.
// Abstract base class
abstract class MyBaseClass {
// Changed abstract properties to private properties
// available in base class only
private string1: string;
private string2: string;
public string3: string;
constructor(string1: string, string2: string) {
// Set private properties in constructor
this.string1 = string1;
this.string2 = string2;
// Set string3 so it is available for rest of class
this.string3 = this.buildString3();
}
protected buildString3(): string {
return this.string1 + " " + this.string2;
}
}
// Concrete subclass
class MySubClass extends MyBaseClass {
constructor() {
// Pass values to base constructor
var string1 = "Hello";
var string2 = "World!";
super(string1, string2);
}
}
// Create object and display string3
var myObject = new MySubClass();
alert(myObject.string3);
This works, but stylistically, I don’t like it for an inheritance-based approach. I’d rather have the ability to simply set properties in the subclass, but call it personal preference. There is nothing wrong with this solution.
Constructor Hook Method
Here, I’ve added an initialize() hook method to the constructor that runs before the buildString3() method. This gives the subclass an opportunity to set properties the base class needs at the appropriate time. I’ve declared the initalize() method as abstract, so that it must be implemented in the subclass.
// Abstract base class
abstract class MyBaseClass {
// Properties to be set in subclass
protected string1: string;
protected string2: string;
public string3: string;
constructor() {
// Call subclass initialize() before string3 is set
this.initialize();
// Set string3 so it is available for rest of class
this.string3 = this.buildString3();
}
abstract initialize(): void;
protected buildString3(): string {
return this.string1 + " " + this.string2;
}
}
// Concrete subclass
class MySubClass extends MyBaseClass {
// Set properties
initialize(): void {
this.string1 = "Hello";
this.string2 = "World!";
}
}
// Create object and display string3
var myObject = new MySubClass();
alert(myObject.string3);
This also works, but it leaves much to be desired. Even though I have declared the initialize() method as abstract, nothing forces the string1 and string2 properties to be set. Notice that I had to remove the abstract keyword from those properties for this to transpile without error. In general, I like the idea of adding hook methods for subclasses to use, but they should be optional. The base class should not depend on them, nor should it be ambiguous about which properties need to be set.
Getters/Setters
As you may have gathered from the above, methods do not suffer from the same constructor timing issues as properties. The base class constructor called into the subclass initialize() method, and it functioned as expected. Likewise, using getter/setter syntax for properties is an option:
// Abstract base class
abstract class MyBaseClass {
// Abstract properties to be set in subclass
// Use getter syntax
protected abstract get string1(): string;
protected abstract get string2(): string;
public string3: string;
constructor() {
// Set string3 so it is available for rest of class
this.string3 = this.buildString3();
}
protected buildString3(): string {
return this.string1 + " " + this.string2;
}
}
// Concrete subclass
class MySubClass extends MyBaseClass {
// Abstract properties must be set or
// transpiler error will occur
// Getter syntax must be used in subclass as well
protected get string1() { return "Hello"; }
protected get string2() { return "World!"; }
}
// Create object and display string3
var myObject = new MySubClass();
alert(myObject.string3);
This is closer to the original vision. Having to use getter syntax is a little wordy for my taste, when all you want to do is return a simple value. You may not mind if you are used to this from other languages.
Move the Code
Finally, my favorite solution is to move the code out of the constructor, which is where the timing issue is. I moved the code into the string3 property with getter syntax. It won’t run until the property is accessed after the object has been constructed, so the timing issue is avoided. I also added a private _string3 property for improved performance, but of course, that is optional.
// Abstract base class
abstract class MyBaseClass {
// Abstract properties to be set in subclass
protected abstract string1: string;
protected abstract string2: string;
// Private backing property (optional)
private _string3: string;
// Public property using getter syntax
public get string3(): string {
// Set private backing property if not already set
if (!this._string3) {
this._string3 = this.buildString3();
}
return this._string3;
}
protected buildString3(): string {
return this.string1 + " " + this.string2;
}
}
// Concrete subclass
class MySubClass extends MyBaseClass {
// Abstract properties must be set or
// transpiler error will occur
protected string1 = "Hello";
protected string2 = "World!";
}
// Create object and display string3
var myObject = new MySubClass();
alert(myObject.string3);
This solution is the closest the original code. I also like the idea of doing more in the base class, so you can do less in subclasses.
Your mileage may vary depending on your specific scenario, so choose the workaround that works best for you.
References