Loading learning content...
When a class inherits from another, it gains access to the parent's members—but this access isn't always automatic or straightforward. The super keyword serves as the explicit bridge between a child class and its parent, enabling controlled access to inherited functionality that might otherwise be hidden, overridden, or ambiguous.
Understanding super is fundamental to working effectively with inheritance. Without it, you cannot call parent constructors, invoke overridden methods, or resolve naming conflicts between parent and child members. It's the mechanism that makes inheritance a collaborative relationship rather than a one-way takeover.
By the end of this page, you will understand the complete mechanics of accessing parent class members through super: when and why you need it, how it differs from regular member access, the rules that govern its usage, and the subtle distinctions that separate correct usage from common mistakes.
Before exploring super, let's understand why we need it. Consider what happens when a child class defines a member with the same name as one in its parent class:
The Shadowing Problem:
When you declare a field or method in a child class with the same name as one in the parent, the child's version shadows (or hides) the parent's. From within the child class, any reference to that name resolves to the child's version—the parent's version becomes inaccessible through normal means.
This creates a fundamental tension in inheritance:
1234567891011121314151617181920212223242526
// The shadowing problem illustratedclass Animal { protected name: string = "Unknown Animal"; speak(): void { console.log(`${this.name} makes a sound`); }} class Dog extends Animal { // This shadows the parent's 'name' field protected name: string = "Unknown Dog"; // This overrides the parent's speak() method speak(): void { // How do we access the PARENT's speak() method? // How do we access the PARENT's name field? console.log(`${this.name} barks`); // Uses Dog's name } // What if we want to call BOTH implementations? speakLoudly(): void { // We need a way to call Animal.speak() from here // this.speak() would call Dog's speak() - infinite possibility of confusion }}If child classes couldn't access parent members that they've overridden, inheritance would be limited to complete replacement rather than extension. You couldn't build on parent behavior—only replace it entirely.
The super keyword is a language mechanism that provides explicit access to the parent class (also called the superclass or base class) from within a child class (also called the subclass or derived class).
What super Represents:
Conceptually, super refers to the parent portion of the current object. When you write super.someMethod(), you're saying "call the version of someMethod() that my parent class defined, not my overridden version."
Critical Distinction:
super is NOT a reference to a different object. The child object and its parent portion are the same object in memory. super is a reference qualifier that tells the compiler/runtime which version of a member to access when multiple versions exist in the inheritance hierarchy.
| Aspect | this Reference | super Reference |
|---|---|---|
| What it refers to | The current object instance | The parent class portion of the current object |
| Method resolution | Starts from the actual runtime type (most derived) | Starts from the immediate parent class |
| Can be used standalone | Yes: return this; | No: super must qualify a member |
| In static context | Not available | Not available |
| For constructor calls | this() calls another constructor in same class | super() calls parent constructor |
The Two Primary Uses of super:
super.member — Access a field or method from the parent classsuper() — Call the parent class constructor (covered in the next page)This page focuses on the first usage: accessing parent class members.
The most common use of super is to call a parent class method from within an overriding method. This pattern enables extension rather than replacement: the child method can add new behavior while preserving and utilizing the parent's behavior.
Pattern: Extending Parent Behavior
When you override a method, you often want to extend what the parent does, not replace it entirely. The super.methodName() call lets you invoke the parent's implementation as part of your extended implementation.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354
// Extending parent behavior with superabstract class Employee { constructor( protected name: string, protected baseSalary: number ) {} calculatePay(): number { // Base implementation: just the base salary return this.baseSalary; } getDetails(): string { return `Employee: ${this.name}`; }} class SalesEmployee extends Employee { private commissionRate: number; private salesAmount: number; constructor(name: string, baseSalary: number, commissionRate: number) { super(name, baseSalary); this.commissionRate = commissionRate; this.salesAmount = 0; } recordSale(amount: number): void { this.salesAmount += amount; } // EXTENDING: Use parent's calculation, then ADD commission calculatePay(): number { const basePay = super.calculatePay(); // Call parent's version const commission = this.salesAmount * this.commissionRate; return basePay + commission; } // EXTENDING: Add sales info to parent's details getDetails(): string { const baseDetails = super.getDetails(); // Get parent's output return `${baseDetails} (Sales), Commission Rate: ${this.commissionRate}`; }} // Usageconst salesRep = new SalesEmployee("Alice", 50000, 0.10);salesRep.recordSale(100000); console.log(salesRep.getDetails());// Output: "Employee: Alice (Sales), Commission Rate: 0.1" console.log(salesRep.calculatePay());// Output: 60000 (50000 base + 10000 commission)When extending parent methods, you can call super before your code (parent runs first), after your code (parent runs last), or in between (wrap parent behavior). The placement depends on your needs—validation often goes before super, cleanup often goes after.
123456789101112131415161718192021222324252627282930313233343536373839404142
// Different patterns for super placementclass Logger { log(message: string): void { console.log(`[${new Date().toISOString()}] ${message}`); }} // PATTERN 1: Call super FIRST (pre-processing)class PrefixLogger extends Logger { constructor(private prefix: string) { super(); } log(message: string): void { super.log(`[${this.prefix}] ${message}`); // Modify input, then delegate }} // PATTERN 2: Call super LAST (post-processing)class CountingLogger extends Logger { private count: number = 0; log(message: string): void { super.log(message); // Do the logging first this.count++; // Then track that it happened } getCount(): number { return this.count; }} // PATTERN 3: Call super IN THE MIDDLE (wrap behavior)class ValidatingLogger extends Logger { log(message: string): void { // Pre-processing: validate if (!message || message.trim().length === 0) { throw new Error("Cannot log empty message"); } // Delegate to parent super.log(message.trim()); // Post-processing: could add notification, metrics, etc. }}While accessing parent methods is the most common use of super, you can also use it to access parent fields—particularly when a child class has shadowed a parent field with a field of the same name.
Field Shadowing vs Method Overriding:
There's an important distinction here that varies by language:
super provides access to the parent's version when both exist.
1234567891011121314151617181920212223242526272829303132
// Accessing parent fields with superclass Vehicle { protected maxSpeed: number = 100; protected description: string = "Generic Vehicle"; getSpecs(): string { return `${this.description} - Max Speed: ${this.maxSpeed} km/h`; }} class SportsCar extends Vehicle { // Shadowing the parent's maxSpeed with a higher value protected maxSpeed: number = 250; protected description: string = "High Performance Sports Car"; getSpecs(): string { return `${this.description} - Max Speed: ${this.maxSpeed} km/h`; } // Method that needs BOTH values getComparison(): string { const parentMaxSpeed = (this as any).__proto__.__proto__.maxSpeed; // Hacky way // Note: TypeScript/JavaScript doesn't support super.field directly for instance fields // This is different from Java where super.fieldName works // In most OOP languages that support it: // const parentMaxSpeed = super.maxSpeed; // Would give 100 // const childMaxSpeed = this.maxSpeed; // Would give 250 return `Standard: ${100} km/h, Sports: ${this.maxSpeed} km/h`; }}Field access via super varies significantly between languages. Java supports super.fieldName for instance fields. TypeScript/JavaScript handles this differently because fields are not truly inherited in the same way. Always check your language's specific rules for field shadowing and super access.
1234567891011121314151617181920
// Java example: super.fieldName works for fieldspublic class Animal { protected String name = "Unknown Animal"; protected int age = 0;} public class Dog extends Animal { protected String name = "Unknown Dog"; // Shadows parent's name public void printNames() { System.out.println("Parent name: " + super.name); // "Unknown Animal" System.out.println("Child name: " + this.name); // "Unknown Dog" } public void printAge() { // age is NOT shadowed, so super.age and this.age are the same System.out.println("Age: " + this.age); System.out.println("Also age: " + super.age); // Same value }}Best Practice: Avoid Field Shadowing
While you can shadow parent fields, it's generally considered poor practice because:
If you need different values, use different names or override accessor methods instead.
Understanding exactly how super resolves member access is crucial for working with complex inheritance hierarchies. The rules are precise and differ from how this works.
Key Resolution Principle:
When you write super.method(), resolution starts from the immediate parent class and proceeds upward through the hierarchy if necessary. This is different from this.method(), which starts from the most derived class (the actual runtime type).
123456789101112131415161718192021222324252627282930313233343536373839404142434445
// Understanding super resolution in a hierarchyclass Grandparent { greet(): string { return "Hello from Grandparent"; } identify(): string { return "I am Grandparent"; }} class Parent extends Grandparent { greet(): string { return "Hello from Parent"; } // Does NOT override identify() - inherits from Grandparent} class Child extends Parent { greet(): string { return "Hello from Child"; } identify(): string { return "I am Child"; } demonstrateSuperResolution(): void { // super.greet() starts from Parent (immediate parent) console.log(super.greet()); // Output: "Hello from Parent" // NOT "Hello from Grandparent" - super always goes to immediate parent // super.identify() starts from Parent // But Parent doesn't override it, so it continues to Grandparent console.log(super.identify()); // Output: "I am Grandparent" // Resolution: Child -> Parent (not found) -> Grandparent (found!) }} // Contrast with this resolutionconst child = new Child();child.greet(); // "Hello from Child" - starts at most derivedchild.identify(); // "I am Child" - starts at most derivedThe parent class for super is determined statically (at compile time) based on the inheritance declaration. This differs from this, which uses dynamic binding (determined at runtime based on the actual object type).
There are specific situations where super is not optional but mandatory for correct program behavior. Understanding these cases prevents subtle bugs.
Mandatory super Use Cases:
123456789101112131415161718192021222324252627282930313233343536373839404142
// Case 1: When overriding lifecycle methodsabstract class Component { private initialized: boolean = false; initialize(): void { // CRITICAL: This sets up base component state this.initialized = true; console.log("Component base initialization complete"); } isReady(): boolean { return this.initialized; }} class CustomButton extends Component { private label: string = ""; initialize(): void { // MUST call super.initialize() or base state is never set super.initialize(); // ← MANDATORY: preserves parent's initialization this.label = "Click me"; console.log("Button-specific initialization complete"); }} const button = new CustomButton();button.initialize();console.log(button.isReady()); // true - because super.initialize() was called // What happens if we forget super.initialize()?class BrokenButton extends Component { initialize(): void { // Forgot to call super.initialize()! console.log("Just my own initialization"); }} const broken = new BrokenButton();broken.initialize();console.log(broken.isReady()); // false - parent was never initialized!Forgetting to call super when overriding methods that perform essential setup is one of the most common inheritance bugs. The code compiles and may appear to work, but invariants are violated. Always ask: 'Does the parent's implementation set up state that I depend on?'
When inheritance goes beyond two levels, super usage becomes more nuanced. Each class in the chain has its own relationship with its immediate parent, and behavior propagates through the entire chain through successive super calls.
The Chaining Effect:
When a child calls super.method(), the parent's implementation might itself call super.method(), which invokes the grandparent's implementation, and so on. This creates a chain of method invocations that can propagate behavior through the entire hierarchy.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748
// Multi-level hierarchy with super chainingclass Widget { render(): string[] { return ["<widget>"]; } cleanup(): void { console.log("Widget cleanup: releasing base resources"); }} class InteractiveWidget extends Widget { render(): string[] { const base = super.render(); // Get Widget's output return [...base, "<interactive-handlers>"]; } cleanup(): void { console.log("InteractiveWidget cleanup: removing event listeners"); super.cleanup(); // Chain to parent's cleanup }} class DraggableWidget extends InteractiveWidget { render(): string[] { const base = super.render(); // Get InteractiveWidget's output return [...base, "<drag-handles>"]; } cleanup(): void { console.log("DraggableWidget cleanup: stopping drag observers"); super.cleanup(); // Chains through InteractiveWidget to Widget }} // The chain in actionconst draggable = new DraggableWidget(); console.log(draggable.render());// Output: ["<widget>", "<interactive-handlers>", "<drag-handles>"]// Each level added its piece through super.render() calls draggable.cleanup();// Output:// "DraggableWidget cleanup: stopping drag observers"// "InteractiveWidget cleanup: removing event listeners" // "Widget cleanup: releasing base resources"// The chain propagates through the entire hierarchyIn a well-designed hierarchy, each level trusts that calling super will handle everything at and above that level. You don't need to know or care about grandparent implementations—the immediate parent handles it. This is the essence of inheritance abstraction.
Why You Can't Skip Levels:
You might wonder why there's no super.super.method() syntax. The answer is fundamental to OOP:
If you find yourself wanting to skip levels, it's usually a sign of a design problem—perhaps the intermediate class shouldn't exist, or the hierarchy needs restructuring.
Even experienced developers make mistakes with super. Let's examine the most common errors and clarify the misconceptions behind them.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354
// Demonstrating common super mistakes // MISTAKE 1: super in static contextclass Base { static greeting(): string { return "Hello from Base"; }} class Derived extends Base { static greeting(): string { // WRONG: Cannot use super in static method in some languages // return super.greeting() + " and Derived"; // CORRECT: Use class name explicitly return Base.greeting() + " and Derived"; }} // MISTAKE 2: Thinking super is another objectclass Animal { speak(): void { console.log("Animal speaks"); console.log("In Animal, this is:", this.constructor.name); }} class Dog extends Animal { speak(): void { super.speak(); // Calls Animal's speak() console.log("Dog barks"); console.log("In Dog, this is:", this.constructor.name); }} const dog = new Dog();dog.speak();// Output:// "Animal speaks"// "In Animal, this is: Dog" ← Same object! this is still the Dog// "Dog barks"// "In Dog, this is: Dog" // MISTAKE 3: Infinite recursion confusionclass Confusing extends Animal { speak(): void { // WRONG: This calls Dog.speak() on this object, not parent! // If someone expected this to call Animal.speak(), they're wrong // this.speak(); ← Would be infinite recursion // CORRECT: Use super to call parent super.speak(); }}When you call super.method(), and that parent method references 'this', it still refers to the current object (the child instance), not a 'parent object'. This is crucial for understanding how polymorphism works even within super calls.
We've explored the mechanics and semantics of using super to access parent class members. Let's consolidate the essential points:
What's Next:
Now that we understand accessing parent members, we'll explore constructor chaining in the next page—how super() is used to ensure proper initialization of the entire inheritance hierarchy from the base class up.
You now understand how to access parent class members using the super keyword. You can invoke overridden methods, understand the resolution rules, and avoid common mistakes. Next, we'll examine constructor chaining to see how super() ensures proper object initialization.