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.
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();
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.
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 thengOnDestroy
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 implementedOne way to do this, which works if the sub classes call
ngOnDestroy
,ngOnDestroy(){ super.ngOnDestroy()}
, or if they don't implementngOnDestroy
at allIn the Base Class