Loading learning content...
When you instantiate a subclass, you're not just creating an object of that subclass—you're creating an object that contains all the layers of its inheritance hierarchy. A Dog object isn't just a Dog; it's also an Animal, which might also be a LivingThing. Each of these layers needs to be properly initialized.
Constructor chaining is the mechanism that ensures every level of an inheritance hierarchy gets its constructor called, in the correct order, so that by the time you receive your new object, it's fully initialized from the base class up.
This isn't just a convenience—it's a requirement. Without constructor chaining, objects would be incompletely initialized, with parent class fields left in undefined or default states, invariants violated, and behavior unpredictable.
This page covers the mechanics of constructor chaining: how to call parent constructors using super(), the rules governing when and how it must be called, patterns for effective constructor design, and common pitfalls to avoid.
To understand why constructor chaining is essential, consider what a constructor actually does:
Constructor Responsibilities:
Now consider inheritance. A child class doesn't just have its own fields—it inherits fields from its parent. Those inherited fields need initialization too. But here's the crucial point: the child class often doesn't know how to initialize the parent's fields.
1234567891011121314151617181920212223242526272829303132333435363738394041
// Why child classes can't initialize parent fields aloneclass Connection { private socket: Socket; private connected: boolean = false; private connectionTime: Date | null = null; constructor(private host: string, private port: number) { // Complex initialization that only Connection understands this.socket = this.createSocket(); this.performHandshake(); this.connected = true; this.connectionTime = new Date(); } private createSocket(): Socket { /* ... */ return {} as Socket; } private performHandshake(): void { /* ... */ }} class SecureConnection extends Connection { private certificate: Certificate; constructor(host: string, port: number, certPath: string) { // PROBLEM: How does SecureConnection initialize: // - socket (it's private to Connection!) // - connected flag (internal logic) // - connectionTime (internal logic) // It can't. It MUST delegate to the parent constructor. super(host, port); // ← Let Connection handle its own initialization // Now handle SecureConnection-specific initialization this.certificate = this.loadCertificate(certPath); this.upgradeToTLS(); } private loadCertificate(path: string): Certificate { /* ... */ return {} as Certificate; } private upgradeToTLS(): void { /* ... */ }} interface Socket {}interface Certificate {}Parent class fields are often private—the child literally cannot access them. Even if they were accessible, duplicating parent initialization logic in every child would violate DRY (Don't Repeat Yourself) and create maintenance nightmares. Constructor chaining preserves encapsulation while enabling proper initialization.
The super() call invokes the parent class constructor from within a child class constructor. It's syntactically similar to a method call, but it has unique rules and behaviors.
Basic Syntax:
constructor(childParams) {
super(parentParams); // Call parent constructor
// Child-specific initialization
}
The arguments passed to super() must match one of the parent class's constructor signatures.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647
// Various super() call patterns // Parent with multiple constructors (overloaded)class Vehicle { protected brand: string; protected year: number; protected color: string; constructor(brand: string); constructor(brand: string, year: number); constructor(brand: string, year: number, color: string); constructor(brand: string, year: number = 2024, color: string = "black") { this.brand = brand; this.year = year; this.color = color; }} class Car extends Vehicle { private numDoors: number; constructor(brand: string, numDoors: number) { // Calling parent with just brand (uses defaults for year, color) super(brand); this.numDoors = numDoors; }} class Motorcycle extends Vehicle { private engineCC: number; constructor(brand: string, year: number, engineCC: number) { // Calling parent with brand and year super(brand, year); this.engineCC = engineCC; }} class CustomCar extends Vehicle { private customFeatures: string[]; constructor(brand: string, year: number, color: string, features: string[]) { // Calling parent with all three parameters super(brand, year, color); this.customFeatures = features; }}| Rule | Java | TypeScript/JavaScript | Python | C# |
|---|---|---|---|---|
| Must be first statement? | Yes | Yes (before 'this') | No (by convention, yes) | Yes (via base()) |
| Implicit if omitted? | Yes (no-arg only) | No (error if parent needs args) | No (error) | Yes (no-arg only) |
| Syntax | super(...) | super(...) | super().init(...) | base(...) |
| Can call with 'this' args? | After super() | After super() | Yes | After base() |
One of the most important rules about constructor chaining is ordering: the parent constructor must be called before the child constructor can use the object.
Why Order Matters:
The object is built from the base class up. Parent fields must be initialized before child fields that might depend on them. Parent methods that the child might call must work correctly, which requires parent initialization to be complete.
The Java/TypeScript Rule:
In Java and TypeScript, super() must be the first statement in the constructor (or at least before any use of this). This isn't arbitrary—it ensures you can't access an uninitialized object.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556
// Why super() must come first (in Java/TypeScript) class Base { protected value: number; constructor() { this.value = 42; console.log("Base constructor: value =", this.value); } getValue(): number { return this.value; }} // CORRECT: super() firstclass CorrectChild extends Base { private multiplied: number; constructor() { super(); // Parent initializes 'value' to 42 // Now it's safe to use inherited members this.multiplied = this.getValue() * 2; // Works: 42 * 2 = 84 }} // INCORRECT: Using 'this' before super() - TypeScript/Java would reject thisclass IncorrectChild extends Base { private multiplied: number; constructor() { // THIS WOULD NOT COMPILE: // this.multiplied = this.getValue() * 2; // ERROR: 'this' before super() // super(); // The problem: getValue() relies on 'value' being initialized, // but we haven't called super() yet, so 'value' is undefined! super(); // Correct position this.multiplied = this.getValue() * 2; }} // What about computation before super()?class ComputeFirst extends Base { private processedInput: string; constructor(input: string) { // You CAN do computation that doesn't involve 'this' const processed = input.trim().toUpperCase(); super(); // OK - the above didn't use 'this' this.processedInput = processed; }}You can perform computation before super() as long as it doesn't access 'this'. This is useful for validating or transforming constructor parameters before passing them to the parent constructor.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950
// Valid pre-super computation for parameter transformation class Logger { constructor(protected logLevel: number) { console.log("Logger initialized with level:", logLevel); }} class FileLogger extends Logger { private filePath: string; constructor(level: string, filePath: string) { // Transform level string to number BEFORE calling super() // This is allowed because it doesn't access 'this' const numericLevel = FileLogger.parseLevel(level); super(numericLevel); // Now pass the transformed value this.filePath = filePath; } // Static method can be called before super() private static parseLevel(level: string): number { const levels: Record<string, number> = { 'debug': 0, 'info': 1, 'warn': 2, 'error': 3 }; return levels[level.toLowerCase()] ?? 1; }} // Parameter validation before super()class ValidatedWidget extends Base { constructor(config: WidgetConfig) { // Validate config before involving 'this' at all if (!config.name || config.name.length === 0) { throw new Error("Widget name is required"); } if (config.width <= 0 || config.height <= 0) { throw new Error("Dimensions must be positive"); } super(); // Only called if validation passes }} interface WidgetConfig { name: string; width: number; height: number;}Some languages insert an automatic (implicit) call to the parent's no-argument constructor if you don't explicitly call super(). This convenience can hide important behavior and lead to confusion.
When Implicit Chaining Applies:
super(), the compiler inserts super() (no-arg) automaticallybase()When Implicit Chaining Fails:
If the parent class doesn't have a no-argument constructor, implicit chaining is impossible, and you must explicitly call super() with the required arguments.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152
// Implicit chaining (when parent has no-arg constructor)class BaseA { protected initialized: boolean = false; constructor() { // No-arg constructor this.initialized = true; console.log("BaseA: no-arg constructor"); }} class ChildA extends BaseA { constructor() { // In Java: super() is implicitly inserted here // In TypeScript: super() is required because we extend a class super(); // Explicit, but Java would add if missing console.log("ChildA: constructor"); }} // Explicit chaining required (parent has no no-arg constructor)class BaseB { protected value: number; constructor(value: number) { // Requires argument this.value = value; console.log("BaseB: constructor with value", value); } // NO no-arg constructor!} class ChildB extends BaseB { private doubled: number; constructor(value: number) { // MUST call super(value) - no implicit option // Compiler error if we omit super() here super(value); this.doubled = value * 2; }} // Error case: forgetting super when requiredclass BrokenChild extends BaseB { constructor() { // ERROR: BaseB has no no-arg constructor // We MUST provide an argument to super() // super(); // This would not compile // CORRECT: Must provide the required argument super(0); // Or some default value }}Relying on implicit super() can make code harder to understand. When reading a child constructor without super(), developers have to remember that the parent's no-arg constructor runs automatically. Many style guides recommend always writing super() explicitly for clarity.
In deeper hierarchies, constructor chaining propagates through every level. Each class calls its immediate parent's constructor, which calls its parent's constructor, until the chain reaches the root class.
The Initialization Order:
super() before doing its own initializationsuper()1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253
// Multi-level constructor chaining demonstrationclass LivingThing { protected alive: boolean; constructor() { console.log("1. LivingThing constructor STARTS"); this.alive = true; console.log("2. LivingThing constructor ENDS"); }} class Animal extends LivingThing { protected species: string; constructor(species: string) { console.log("3. Animal constructor STARTS (before super)"); super(); // Calls LivingThing constructor console.log("4. Animal constructor CONTINUES (after super)"); this.species = species; console.log("5. Animal constructor ENDS"); }} class Dog extends Animal { private breed: string; constructor(breed: string) { console.log("6. Dog constructor STARTS (before super)"); super("Canis lupus familiaris"); // Calls Animal constructor console.log("7. Dog constructor CONTINUES (after super)"); this.breed = breed; console.log("8. Dog constructor ENDS"); }} // Creating a Dogconst dog = new Dog("Labrador"); /* Output order (note the nesting):6. Dog constructor STARTS (before super)3. Animal constructor STARTS (before super)1. LivingThing constructor STARTS2. LivingThing constructor ENDS4. Animal constructor CONTINUES (after super)5. Animal constructor ENDS7. Dog constructor CONTINUES (after super)8. Dog constructor ENDS*/ // At each step:// - dog.alive: set by LivingThing// - dog.species: set by Animal // - dog.breed: set by DogWhen child classes need to pass parameters to parent constructors, you must design the parameter flow carefully. The child's constructor parameters often include values destined for the parent, plus child-specific values.
Common Patterns:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273
// PATTERN 1: Pass-through parameters// Child receives parent's parameters and passes them upclass Shape { constructor( protected x: number, protected y: number, protected color: string ) {}} class Rectangle extends Shape { constructor( x: number, // For parent y: number, // For parent color: string, // For parent private width: number, // For self private height: number // For self ) { super(x, y, color); // Pass parent's params }} // PATTERN 2: Derived parameters// Child computes parent parameters from its own valuesclass Container { constructor(protected capacity: number) {}} class Box extends Container { constructor( private width: number, private height: number, private depth: number ) { // Compute capacity from dimensions super(width * height * depth); }} // PATTERN 3: Configuration object pattern// Single config object split between parent and childinterface BaseConfig { name: string; enabled: boolean;} interface ExtendedConfig extends BaseConfig { customOption: string; retryCount: number;} class BaseService { protected name: string; protected enabled: boolean; constructor(config: BaseConfig) { this.name = config.name; this.enabled = config.enabled; }} class ExtendedService extends BaseService { private customOption: string; private retryCount: number; constructor(config: ExtendedConfig) { // Pass the base portion to parent super({ name: config.name, enabled: config.enabled }); // Handle extended portion this.customOption = config.customOption; this.retryCount = config.retryCount; }}When constructors have many parameters, the configuration object pattern scales better than long parameter lists. It's self-documenting, allows optional parameters with defaults, and makes it easier to add new parameters without breaking existing callers.
Constructor chaining has several pitfalls that can cause subtle bugs or compilation errors. Let's examine the most common mistakes.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455
// MISTAKE: Calling overridable method in constructorclass Animal { protected sound: string = ""; constructor() { // DANGEROUS: Child may not be initialized yet! this.makeSound(); // Calls the overridden version } makeSound(): void { console.log("Some animal sound"); }} class Cat extends Animal { private meowCount: number; constructor() { super(); // Problem: super() calls makeSound() this.meowCount = 0; // This runs AFTER makeSound()! } makeSound(): void { this.meowCount++; // BUG: meowCount is undefined here! console.log("Meow! Count:", this.meowCount); }} const cat = new Cat();// Output: "Meow! Count: NaN" (meowCount was undefined) // SOLUTION: Use factory pattern for initialization that needs overridesclass SafeAnimal { protected sound: string = ""; // Constructor does minimal, safe initialization constructor() { // Don't call overridable methods here } // Factory method that can safely call overridable methods static create<T extends SafeAnimal>(this: new () => T): T { const instance = new this(); instance.initialize(); // Called after construction is complete return instance; } protected initialize(): void { this.makeSound(); // Safe to override now } makeSound(): void { console.log("Some animal sound"); }}Calling overridable methods from a constructor is one of the most dangerous patterns in OOP. The child's method runs before the child's constructor completes, accessing uninitialized fields. Always prefer private or final/non-overridable methods in constructors.
Besides super() for calling parent constructors, many languages support this() for calling another constructor in the same class. This enables constructor overloading with shared initialization logic.
The Pattern:
Specialized constructors (with fewer parameters) delegate to more general constructors (with more parameters), providing default values. The most general constructor does the actual initialization work.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263
// Constructor chaining within the same class (Java-style)// Note: TypeScript doesn't support this() syntax, showing Java equivalent // JAVA EXAMPLE:/*public class Rectangle { private double width; private double height; private String color; // Most general constructor - does the real work public Rectangle(double width, double height, String color) { this.width = width; this.height = height; this.color = color; } // Delegates to the general constructor with default color public Rectangle(double width, double height) { this(width, height, "black"); // this() call } // Delegates to create a square public Rectangle(double size) { this(size, size, "black"); // this() call } // Default constructor public Rectangle() { this(1.0, 1.0, "black"); // this() call }}*/ // TypeScript approach: Use optional parameters or factory methodsclass Rectangle { private width: number; private height: number; private color: string; // Single constructor with optional parameters constructor(width: number = 1, height?: number, color: string = "black") { this.width = width; this.height = height ?? width; // Square if height not provided this.color = color; } // Alternative: Factory methods for different creation patterns static createSquare(size: number, color?: string): Rectangle { return new Rectangle(size, size, color); } static createDefault(): Rectangle { return new Rectangle(); }} // Usageconst rect1 = new Rectangle(10, 20, "red"); // Full specificationconst rect2 = new Rectangle(10, 20); // Default colorconst rect3 = new Rectangle(10); // Square with default colorconst rect4 = new Rectangle(); // All defaultsconst square = Rectangle.createSquare(5); // Factory method| Aspect | this() | super() |
|---|---|---|
| Purpose | Call another constructor in same class | Call parent class constructor |
| When used | Constructor overloading | Inheritance hierarchy initialization |
| Position | Must be first statement | Must be first statement |
| Can both be used? | No—only one can be first | No—only one can be first |
| What happens to parent? | Delegated constructor handles super() | Directly calls parent constructor |
Constructor chaining ensures proper initialization of the entire inheritance hierarchy. Let's summarize the key principles:
What's Next:
Constructor chaining ensures initialization. But inheritance is also about behavior—calling parent methods from child methods. In the next page, we'll explore method chaining with super, seeing how overriding methods can extend rather than replace parent behavior.
You now understand constructor chaining—the mechanism that ensures every level of an inheritance hierarchy is properly initialized. You know when super() is required, the ordering rules, and how to avoid common mistakes. Next, we'll see how super enables method chaining for behavior extension.