1

I'm trying to extend/override a protected method in a class we'll call ChildClass a class library's protected override method tapNode within a ParentClass, whose method calls super to the another protected override method tapNode in a GrandParentClass.

I want to override the behavior such that ChildClass can call the grandParentClass while extending from the ParentClass.

To clarify, we'd have

export class ChildClass extends ParentClass {
  override tapNode(node?: TreeNode): void {
    custom_stuff();
    super.super.tapNode(node); //What I ideally want to do but can't
}
export class ParentClass extends ChildClass {
  override tapNode(node?: TreeNode): void {
    [ ...
      inline class-specific code
    ... ]
    super.tapNode(node);
}
export class GrandParentClass extends ParentClass {
  override tapNode(node?: TreeNode): void {
    [ ...
      inline class-specific code
    ... ]
    super.tapNode(node)
}

Some approaches I've looked at so far:

  • I'm aware of how one can use the prototype method, but this seemingly only applies on public methods, NOT protected methods. (see TypeScript super.super call for more info on that approach)

  • I'm aware of mixins and ts-mixer, but that seemingly only works if there's unique method names since you're doing a composition of classes. (see Typescript: How to extend two classes? )

  • I'm aware of the idea of overriding the class-specific code if it's put into it's own method, but that only applies when the code is separated out into it's own method, NOT when it's inline. (see https://stackoverflow.com/a/56536651/314780 as an example).

  • I'm aware you generally don't want to do this!

Vivek Gani
  • 1,283
  • 14
  • 28

1 Answers1

1

As you figured out, the class inheritance in TypeScript (and ECMAScript, at least at the time of writing) does not support this use case.

As you also found out, a possible workaround is to fallback to the underlying prototype. However, doing so for a protected method raises an error with TypeScript (as you pointed out, TS allows this for public methods only).

That being said, the emitted JS code still contains the protected (and even private!) methods on the prototype, since the visibility modifiers are only at TS level:

Like other aspects of TypeScript’s type system, private and protected are only enforced during type checking.

This means that JavaScript runtime constructs like in or simple property lookup can still access a private or protected member

Therefore, ignoring the TS error, the runtime works as expected:

class GrandParentClass {
    protected tapNode(node?: TreeNode): void {
        /* ...
          inline class-specific code
        ... */
        console.log("GrandParent", node)
    }
}

class ParentClass extends GrandParentClass {
    override tapNode(node?: TreeNode): void {
        /* ...
          inline class-specific code
        ... */
        console.log("Parent", node)
        super.tapNode(node);
    }
}

class ChildClass extends ParentClass {
    override tapNode(node?: TreeNode): void {
        //custom_stuff();
        //super.super.tapNode(node); //What I ideally want to do but can't

        console.log("Child", node)

        // @ts-ignore In order to break the class inheritance contract,
        // we have to break the TS visibility modifier contract as well...
        GrandParentClass.prototype.tapNode.call(this, node)
        //                         ~~~~~~~ Error: Property 'tapNode' is protected and only accessible through an instance of class 'ChildClass'. This is an instance of class 'GrandParentClass'.
    }
}

new ChildClass().tapNode("some tree node")

type TreeNode = string 

...correctly outputs:

[LOG]: "Child",  "some tree node" 
// (No Parent log)
[LOG]: "GrandParent",  "some tree node" 

Playground Link

ghybs
  • 47,565
  • 6
  • 74
  • 99
  • Thanks, the key parts I learned here as you mention was putting in a @ts-ignore for this scenario, and using the `.call` method passing in both the instance and other parameters in. – Vivek Gani Sep 23 '22 at 19:15