Execution Order and Solutions for Calling Overridden Methods in Base Class Constructors in TypeScript

Dec 06, 2025 · Programming · 9 views · 7.8

Keywords: TypeScript | constructor | method overriding | execution order | inheritance

Abstract: This article provides an in-depth analysis of the issue where subclass properties remain uninitialized when base class constructors call overridden methods in TypeScript. By examining the constructor execution order in JavaScript/TypeScript, it explains why accessing subclass properties in overridden methods results in undefined values. The paper details the constructor chaining mechanism, presents multiple solutions including delayed invocation in subclass constructors, factory method patterns, and parameter passing strategies, and compares the applicability of different approaches in various scenarios.

Problem Background and Phenomenon Analysis

In object-oriented programming, method overriding is a core feature of inheritance mechanisms. However, in TypeScript (and JavaScript), developers often encounter a perplexing issue when base class constructors call methods that are overridden by subclasses: subclass properties are not yet initialized, resulting in undefined values when these properties are accessed within the overridden methods.

Consider the following typical example:

class A {
    constructor() {
        this.MyvirtualMethod();
    }

    protected MyvirtualMethod(): void {
        // Base class implementation
    }
}

class B extends A {
    private testString: string = "Test String";

    public MyvirtualMethod(): void {
        console.log(this.testString); // Output: undefined
    }
}

When new B() is executed, the console outputs undefined instead of the expected "Test String". The root cause of this problem lies in constructor execution order, not in any defect in TypeScript's type system or method overriding mechanism.

Underlying Mechanism of Execution Order

To understand this issue, it's essential to analyze the execution order after TypeScript is compiled to JavaScript. When creating an instance of class B, the actual steps are as follows:

  1. First, the base class A constructor executes
  2. Within A's constructor, this.MyvirtualMethod() is called
  3. Due to polymorphism, the overridden MyvirtualMethod() from class B is actually invoked
  4. At this point, class B's constructor has not yet executed, so the testString property has not been initialized to "Test String"
  5. Finally, class B's constructor executes, completing the assignment of testString

This process becomes clearer when examining the compiled JavaScript code:

function B() {
    _super.apply(this, arguments);   // MyvirtualMethod is called here
    this.testString = "Test String"; // Property assignment occurs afterward
}

This execution order is standard behavior for JavaScript engines, and TypeScript, as a superset of JavaScript, naturally follows the same rules. The key insight is that property initialization occurs in the subclass constructor, while the base class constructor executes before the subclass constructor.

Solutions and Best Practices

Solution 1: Delayed Invocation in Subclass Constructor

The most straightforward solution is to move the critical method call from the base class constructor to the subclass constructor:

class A {
    protected initialize(): void {
        this.MyvirtualMethod();
    }

    protected MyvirtualMethod(): void {
        // Base class implementation
    }
}

class B extends A {
    private testString: string = "Test String";

    constructor() {
        super();
        this.initialize(); // Ensure properties are initialized before calling
    }

    protected MyvirtualMethod(): void {
        console.log(this.testString); // Correctly outputs: "Test String"
    }
}

This approach introduces a dedicated initialization method that postpones the execution of critical logic until after the subclass constructor has completed property initialization.

Solution 2: Factory Method Pattern

For scenarios requiring more complex initialization logic, the factory method pattern can be employed:

abstract class A {
    protected abstract MyvirtualMethod(): void;
    
    public static createInstance<T extends A>(ctor: new () => T): T {
        const instance = new ctor();
        instance.MyvirtualMethod();
        return instance;
    }
}

class B extends A {
    private testString: string = "Test String";

    protected MyvirtualMethod(): void {
        console.log(this.testString); // Correct output
    }
}

// Create instance using factory method
const b = A.createInstance(B);

The factory method pattern separates object creation from initialization, ensuring that all properties are properly initialized before overridden methods are called.

Solution 3: Constructor Parameter Passing

Pass necessary initialization data through constructor parameters:

class A {
    constructor(protected initialValue?: string) {
        if (initialValue) {
            this.MyvirtualMethod(initialValue);
        }
    }

    protected MyvirtualMethod(value: string): void {
        console.log("Base:", value);
    }
}

class B extends A {
    private testString: string;

    constructor(value: string = "Test String") {
        super(value);
        this.testString = value;
    }

    protected MyvirtualMethod(value: string): void {
        console.log("Derived:", value); // Can access the correct value
    }
}

Design Principles and Considerations

When addressing this issue, several important design principles should be considered:

  1. Avoid calling overridable methods in constructors: This is the fundamental design principle. The primary responsibility of constructors should be initializing the object's internal state, not containing complex business logic.
  2. Define clear initialization phases: For objects requiring complex initialization, consider dividing the initialization process into multiple phases, ensuring properties are fully initialized before executing business logic.
  3. Use abstract methods for constraints: Declare critical methods as abstract in base classes to force subclasses to provide implementations, while documenting requirements for invocation timing.
  4. Consider initialization hooks: Define dedicated initialization hook methods, such as afterInitialize(), that are automatically called after objects are fully constructed.

Comparison with Other Languages

It's important to note that this issue is not unique to TypeScript. In languages like C++ and Java, similar problems can occur when virtual methods are called in constructors, though the specific manifestations may vary due to language characteristics:

These differences remind us that when developing across languages or learning new ones, we need to pay particular attention to the specific implementation details of their constructor and inheritance mechanisms.

Conclusion

The issue of subclass properties remaining uninitialized when base class constructors call overridden methods in TypeScript stems from the constructor execution order in JavaScript engines. By understanding this mechanism, developers can employ various strategies to avoid this problem:

  1. Move critical method calls from constructors to dedicated initialization methods
  2. Use factory method patterns to separate object creation from initialization
  3. Pass necessary initialization data through constructor parameters
  4. Follow the design principle of "avoid calling overridable methods in constructors"

Each solution has its applicable scenarios, and developers should choose the most appropriate method based on specific requirements. Most importantly, understanding the underlying mechanisms and following good object-oriented design principles enables the creation of robust, maintainable TypeScript code.

Copyright Notice: All rights in this article are reserved by the operators of DevGex. Reasonable sharing and citation are welcome; any reproduction, excerpting, or re-publication without prior permission is prohibited.