javascripttypescriptes6-class

Why does setting properties on ES6 classes not work when calling an overloaded function in the constructor of the parent


I was writing code and came across this scenario I can't wrap my head around.

The scenario is the following: I have a class from a library which I am extending. This class is the "Parent"-class. It allows it's subclasses to overwrite the init-method in order to do custom initialization.

The second child class however does not behave like I'd expect. The only difference between both classes is the declaration(?) of the member variable. When writing pure JS I would not even consider doing this, but I'm writing in Typescript and this is the compiled result (actually, I'm not even assigning a value in my specific case. The TS compiler just includes a "member;" and the output does read "undefined)

I'm pretty sure this problem only occurs, when the variable is set in an overwritten function that is called by the parent constructor.

Can someone explain to me why this is happening?

class Parent {
    constructor(initArgs) {
        this.init(initArgs);
    }

    init() {}
}

class ChildTest1 extends Parent {
    init(args) {
        this.member = args;
    }

    test() {
        console.log(`Member of ChildTest1 has value of "${this.member}"`);
    }
}

class ChildTest2 extends Parent {
    member = "default";

    init(args) {
        this.member = args;
    }

    test() {
        console.log(`Member of ChildTest2 has value of "${this.member}"`);
    }
}

new ChildTest1("Hello World").test();
new ChildTest2("Hello World").test();

Outputs the following:

Member of ChildTest1 has value of "Hello World"

Member of ChildTest2 has value of "default"


Solution

  • Your variable is overwritten during initialization. The order of initialization of properties and constructor execution is as follows:

    1. The constructor of your parent class executes and initializes member to Hello World

    2. Your child class properties are initialized, overwriting the previously initialized member variable.

    3. Your child class constructor executes

    I've made your code more explicit (note that adding the child constructor is functionally equivalent to your example) to demonstrate this:

    class Parent {
        constructor(initArgs) {
            this.init(initArgs);
            console.log(`Parent Constructor: Member of ChildTest2 has value of "${this.member}"`);
        }
    
        init() {}
    }
    
    class ChildTest2 extends Parent {
        member = "default";
    
        constructor(initArgs) {
            super(initArgs);
            console.log(`Child Constructor: Member of ChildTest2 has value of "${this.member}"`);
        }
    
        init(args) {
            this.member = args;
            console.log(`Init: Member of ChildTest2 has value of "${this.member}"`);
        }
    
        test() {
            console.log(`Test: Member of ChildTest2 has value of "${this.member}"`);
        }
    }
    
    new ChildTest2("Hello World").test();
    

    This prints:

    [LOG]: "Init: Member of ChildTest2 has value of "Hello World"" 
    [LOG]: "Parent Constructor: Member of ChildTest2 has value of "Hello World"" 
    [LOG]: "Child Constructor: Member of ChildTest2 has value of "default"" 
    [LOG]: "Test: Member of ChildTest2 has value of "default""
    

    As a mental model, you can assume that class property initialization happens after the super(...) call inside the constructor. More on that here