Typescript: keyword to force calling the super on any method

Created on 24 Jan 2018  Ā·  25Comments  Ā·  Source: microsoft/TypeScript

Today I faced the following code

class A {
 // silly warning comment: if you override this, don't forget to call the super method to avoid memory leaks
onExit() {
// do some important cleaning stuff
}
}

class B extends A {
onExit() {
super.onExit(); // good
}
}

class C extends A {
onExit() {
// forgot to call to super.onExit = memory leaks
}
}

The problem is that, unlike a constructor, there is no way to force a method overriding another one to call the parent "super" function.

I wished we had a "concrete"* keyword to let the user know he must call the super method.

class A {
concrete onExit() {
// do some cleaning stuff
}
}

class B extends A {
onExit() {
super.onExit(); // no error
}
}

class C extends A {
 // error: Declaration of derived method must contain a 'super' call
onExit() {
}
}

In another language, I could have used the final keyword to prevent overriding the method but thenā€¦ no overriding allowed neither.

  • "concrete" In opposition to "abstract" (for lack of a better name), other ideas: "important" or "mandatory"
Awaiting More Feedback Suggestion

Most helpful comment

@Xample I had the same problem with trying to ensure that classes that extended a BaseEventService class called super.ngOnDestroy if they override the ngOnDestroy in their implementations. I couldn't find a way to enforce it with warnings/TypeScript constraints, but I can ensure that the functions in the Base Class get called regardless of how the sub classes are implemented

One way to do this, which works if the sub classes call ngOnDestroy, ngOnDestroy(){ super.ngOnDestroy()}, or if they don't implement ngOnDestroy at all

In the Base Class

    constructor() {
        super();
        let originalOnDestroy = this.ngOnDestroy;
        this.ngOnDestroy = () => {
            this.destroy();
            originalOnDestroy.apply(this);
        };
    }

    ngOnDestroy(): void {
        // do nothing
    }

    private destroy() {
        // clears all subscriptions etc
    }

All 25 comments

Instead of requiring overriders to remember to call super, you could do a design like so:

class A {
    final exit() {
        // do some important cleaning stuff
    }

    /** Override me to your heart's content. */
    onExit() {}
}

class B extends A {
    exit() { /* forget to call super() */ } // Errt, not allowed, no overriding 'exit'

    onExit() { } // Write whatever you want, the important cleanup will be done in 'exit'
}

If we had #1534. Would that be sufficient for your use case?

ā€œfinalā€ or ā€œsealā€ (as in #1534) are not sufficient. I have no hand on who calls exit(). Imagine the angular 4ā€™s ā€œonDestroyā€ hook, if I want to make a baseComponent implementing the ā€œonDestroyā€ and to extend this component, I must remember my parentā€™s to already have itā€™s onDestroy. The framework is calling the method, the interface is given by the framework, therefore I cannot rename it.

If you can't rename it, you presumably can't add the concrete modifier to it either?

The framework is giving you the interface. You would add concrete on the class implementation (not on the interface).

A workaround: Declare that onExit() returns a "receipt" that proves it was run on the superclass, and make sure that the only (reasonable) way to get such a receipt is by calling super.onExit(). In the following case, ExitReceipt is a private static member of A: so methods of A can refer to the value A.Receipt, but methods of B and C cannot. And the type of A.ExitReceipt is an instance of an anonymous class with a private member, so you can't easily get a new one.

class A {  
  private static ExitReceipt = new (class {
    private property: string = "";
  })();

  onExit(): typeof A.ExitReceipt {
    // do some important cleaning stuff
    return A.ExitReceipt;
  }
}

class B extends A {
  onExit() {
    // do some before-cleanup stuff
    const exitReceipt = super.onExit();
    // do some after-cleanup stuff
    return exitReceipt;
  }
}
class C extends A {
  // doesn't work since it doesn't return a receipt  
  onExit() { 
  }
}

Does that make sense?

Which other languages that support OOP have this construct?

I found the @CallSuper decorator in Java
https://developer.android.com/reference/android/support/annotation/CallSuper.html

usage

@CallSuper
  public abstract void onFocusLost();

From this initial question

But in this implementation (to my understanding), the @CallSuper decorator forces the method to call "super". In my case, any subclass should implicitly inherit this @CallSuper decorator. In their Java example, they ensure this behavior adding the abstract keyword.

@jcalz interesting hack but I don't have the hand on the interface, only the implementation

@Xample I find the construct interesting, in that it enhances the contract between classes of the inheritance tree. I don't recall such a construct in other OO programing languages.

I think the keyword semantic should refer to some idea of nesting, since each descending class in the inheritance tree has to call the upper super method... Perhaps nested or onion. "mandatory" sounds like the implementation is mandatory (like "abstract") and "concrete" is weird in that the opposite of abstract would rather be "any defined method", which is orthogonal to this construct.

@sveinburne english not being my mother tongue, its hard for me to find a keyword which really sounds good. What I can think about is that the function should have a name referencing that it will always be called i.e. we cannot shadow this method. You want to call it "bright" ? or maybe "root" as you refer to a tree ?

I think your problem can be resolved with this:

class A {
  // this is private then is not overridable
  private onExit() {
    // do some important cleaning stuff
    // call overridable method empty in base class
    this.OnExit();
  }
  OnExit() { } // or abstract
}

class B extends A {
  OnExit() {
  }
}

}

@dardino it's a solution if it's your own code and you do not rely on interfaces. "onExit" being part of an interface it cannot be private. The real life use of my proposal is mostly for hooks. Like we see in angular or [ionic] (https://ionicframework.com/docs/nightly/api/navigation/NavController/#lifecycle-events) Lifecycle events (but not restricted to).

let's take the following example:

export class BaseComponent implements OnDestroy {
  ngOnDestroy() { 
 // clean up stuff
 }
}

where OnDestroy is as follow:

export interface OnDestroy {
    ngOnDestroy(): void;
}

now imagine I would extend this class

export class AClass extends BaseComponent {
  ngOnDestroy() { 
 // problem if I do not call the super ngOnDestroy() function
 }
}

A "mandatory" keyword would print an error if we forget to call the super method. (as it is already the case with constructors of class extending another one)

@Xample I had the same problem with trying to ensure that classes that extended a BaseEventService class called super.ngOnDestroy if they override the ngOnDestroy in their implementations. I couldn't find a way to enforce it with warnings/TypeScript constraints, but I can ensure that the functions in the Base Class get called regardless of how the sub classes are implemented

One way to do this, which works if the sub classes call ngOnDestroy, ngOnDestroy(){ super.ngOnDestroy()}, or if they don't implement ngOnDestroy at all

In the Base Class

    constructor() {
        super();
        let originalOnDestroy = this.ngOnDestroy;
        this.ngOnDestroy = () => {
            this.destroy();
            originalOnDestroy.apply(this);
        };
    }

    ngOnDestroy(): void {
        // do nothing
    }

    private destroy() {
        // clears all subscriptions etc
    }

The keyword could be required.

@lukescott like required in swift ?
https://stackoverflow.com/questions/26923123/what-does-the-required-keyword-in-swift-mean

In swift it means the subclass needs to implement that method one (a little bit like ā€˜abstractā€™ keyword) not that he subclassā€™s method have to call the super one.

That would be a very nice feature in addition to TypeScript 2.2 mixin syntax. For example, I have a mixin that defines componentWillUnmount method for React.Component which performs some kind of destruction finalization. Than I implement a class that extends this mixin and implements own componentWillUnmount - it is very easy to miss super.componentWillUnmount call that may lead to bugs that are hard to track.

We are working with Ionic/Angular, in our project there are a couple of base classes that implements standard functions centralised for all others inherited classes (pages/services/etc)

Using these frameworks is rather frequent to override methods (hooks) that let us control the lifecycle of components, pages, etc.

It is a real pain have to manually check that in each method override there is a proper calls to super().

Definitively I add my vote for this feature request, a simple keyword like required or mustcall to let the compiler emit an alert on the console could be enough.

@RyanCavanaugh:

Which other languages that support OOP have this construct?

For example, Java and C# does not suffer from this problem, because they require to explicitly use override keyword on order to override methods.

public class Base
{
    public virtual void A() => Console.WriteLine("Base.A");
    public virtual void B() => Console.WriteLine("Base.B");
} 
public class Concrete: Base
{
    public override void A() { } //thanks override keyword developers are aware of the base.A() method

    public void B() {} //throws an error or warning that Concrete.B hides Base.B

    public new void B() => Console.WriteLine("Cocnrete.B") // keywork new explicitelly hides Base.B
} 

Concrete concrete = new Concrete();
concrete.B(); //writes Concrete.B
Base base_ = concrete;
base.B(); //writes Base.B

However in typescript, it can very easily happed that a method is overridden unintentionally. Think of following inheritance

class Base {}
class Concrete1 extends Base
{
   ngOnDestroy() { console.log("Concrete1.ngOnDestroy" }
}

this is proper inheritance. However, if at some point we would add ngOnDestroy to Base class, it would render all extended classes invalid, since they would override Base.onOnDestroy() automatically.

@Liero
ā€œI assume concrete extends Baseā€
As you wrote, ā€œoverrideā€ only explicitly tell the compiler that the coder knows what he is doing. I have no problem of overriding a method even without the ā€œoverrideā€ keyword, my problem is more to ensure a super method to be called (like it is the case for a constructor). ā€œoverrideā€ is another interredting feature btw

Is this summary correct?

| | C++ | C# | TypeScript | Typescript proposed additions for non-breaking change | Opinionated perfect design? |
|-----------|----------------------------------------------------------------------------|---------------------------------------------------------|------------------------------|-------------------------------------------------------|------------------------------------------------------------------------------------------------------|
| on class | final, virtual | sealed, abstract | abstract | final/sealed | enabled some sort of 'extendable' keyword on class |
| defaults | unspecified (lol) | virtual class | 'virtual'(proto based) class | 'virtual' | sealed, adding virtual methods makes classes virtual, 'extendable' classes |
| | | | | | |
| on method | final, virtual, override, 'abstract', no 'callbase' on virtual, final, override | sealed, virtual, override, new | abstract | final/sealed, call-super/callsuper/call_super | enabled keywords: virtual, abstract, override, final/sealed, callbase/callsuper even on non-virtual methods |
| defaults | 'could-be-shadown', 'could-shadow', 'non-virtual' | 'could-be-shadown', 'could-not-shadow', 'could-virtual' | 'virtual' | 'virtual' | 'non-virtual' but 'could-shadow' with new and 'could-be-shadown' |

concrete keyword imho is confusing in naming with virtual which all methods in TypeScript (kind of) are. Let's call it callsuper or required-super and be done with it lol.

@Xample I feel like in the protected override method() case, the compiler would not throw if super.method() was called in the child

example (using angular hook for demo purposes):

class Parent implements OnInit { 

  public ngOnInit(): void {
    console.log('in parent ngOnInit');
  }

}


class ChildA extends Parent {

  public ngOnInit(): void { // should throw
    console.log('in child ngOnInit');
  }

}

class ChildB extends Parent {

  public ngOnInit(): void { // should NOT throw
    super.ngOnInit();
    console.log('in child ngOnInit');
  }

}

class ChildC extends Parent {

  public override ngOnInit(): void { // should NOT throw
    console.log('in child ngOnInit');
  }

}

class ChildD extends Parent {

  public override ngOnInit(): void { // should throw ?
    super.ngOnInit();
    console.log('in child ngOnInit');
  }

}

This option feels pretty clean however it would be a breaking change for typescript which might make it a far more difficult proposal

Landed here looking for this feature as well. For what it's worth, this is also implemented in iOS Objective-C using the NS_REQUIRES_SUPER compiler macro, which annotates a function such that a subclass overriding the function that fails to call super.thatFunction() will error at compile time.

My main reason for wanting this function is to enable a seamless insertion functionality without the awkward renaming of common well-known functions. When dealing, for example, with the componentDidMount function in React I think it's most graceful to be able to write:

class Screen extends React.Component {
  require_super componentDidMount() {
    // ... Do something like logging that the Screen appeared
  }
}

class LoginScreen extends Screen {
  componentDidMount() {
    super.componentDidMount(); // Must call or would cause a TSC error
    // ... Do something else like animate in display
  }
}

I'd much rather that solution than to have to use final to try and lock down the componentDidMount() function name at the Screen class and then require new developers coming on to the project to remember that only when dealing with Screens they actually have to call some project-specific analogous function like thisScreenDidMount() that is open for overriding. (Though I acknowledge that doing so would achieve the same goal and is, indeed, what Swift, Objective-C's successor language, has opted for as the solution for this problem.)

@zakhenry I second this proposal. I think it could catch bugs before they're written in a number of cases, not just with component lifecycle events

I think the concern about being a breaking change could be addressed by an option in tsconfig.json, perhaps? Something analogous to strictNullChecks, like strictOverrideChecks?

I suggest typescript add annotation like Java.

class Parent {
    @supercall  // overriding methods with this annotation must call their super methods first, or tsc will throw an error.
    protected onCreate() {
        // ...
    }
}

class Child {
    protected onCreate() {  // tsc will throw supercall violation error
        // does not call the overriden method
    }
}

With this approach, typescript doesn't need to consider itself being messed up in grammar. And this annotation feature will open other additional features in way easier and unified way in typescript.

I worry that overloading the @ annotation to represent both compile-time annotations as well as Decorators would create confusion and bugs.

Was this page helpful?
0 / 5 - 0 ratings