Search code examples
javascripttypescriptinheritancesubclassprivate-members

How do you call a subclass method in the baseclass method?


I am sorry if this question is so confusing, I am going to try and be as simple as possible.

TLDR; How, in a Typescript baseclass, do you call the private method of a subclass that has to be defined in the subclass?

I have many different Typescript subclasses that require different code to do the same thing: write to a file.

90% of the base code is the same, with the specific implementation of some methods per subclass.

So I have taken the same code and put it in a base class, but that calls a method that has to be implemented on the subclass and it is preferably private.

Here is an example:

abstract class BaseClass {
  constructor() { }

  SetOptions() {
    // Set the options
    // Return the options
  }

  GetOptions() {
    // Get the options
    this.WriteStuff();
  }

  private WriteStuff(arg1: Item, arg2: number) {}
}

class SubClass1 extends BaseClass {
  private WriteStuff(arg1: SubClass1Item, number: number) {
    // Write stuff in the SubClass1 way
  }
}

class SubClass2 extends BaseClass {
  private WriteStuff(arg1: SubClass2Item, number: number) {
    // Write stuff the SubClass2 way
  }
}

SubClass1Item and SubClass2Item implement the Item interface.

All subclasses will use the same code for SetOptions and GetOptions, but WriteStuff will have individualized code in each subclass and needs to be private so it isn't called by other devs on accident.

Typescript says that I can't do this because "Types have separate declarations of a private property 'WriteStuff'". And Typescript won't let me not declare the method in the BaseClass as then I am calling a method that doesn't exist but will exist in the subclass.

Is this possible?


Solution

  • Private vs. Protected

    private methods can only be accessed in the class itself, not in any descendants. You cannot have a private abstract method because this is an inherent contradiction -- if descendants need to implement it then it is not private.

    You want to use the protected modifier. Per the docs:

    The protected modifier acts much like the private modifier with the exception that members declared protected can also be accessed within deriving classes.

    You want to declare in your BaseClass that all concretions must implement a WriteStuff() method. This is an abstract method meaning that it has no implementation in BaseClass and must be overwritten.

    In your descendants, the implementation of WriteStuff must be protected rather than private.

    Code

    abstract class BaseClass {
      /* .... */
      protected abstract WriteStuff(arg1: Item, arg2: number): void;
    }
    
    class SubClass1 extends BaseClass {
      protected WriteStuff(arg1: SubClass1Item, number: number) {
        // Write stuff in the SubClass1 way
      }
    }
    

    Playground Link

    arg1 Type

    Your classes don't all share an identical signature because they each have a different type for the first argument arg1 passed to WriteStuff. You most likely want to use a generic class based on the type of arg1 or some other value.

    If SubClass1Item and SubClass2Item do not extend Item then you absolutely need a generic class because the implementations of WriteStuff in the subclasses would not be assignable to the base.

    If they do extend Item then you will not get typescript errors with the current setup. However there is potential for runtime errors when you are calling this.WriteStuff from GetOptions() in BaseClass. How does the BaseClass know if the Item that it has is assignable to the Item subtype that is expected in SubClass1 or SubClass2? This is where generics can help.

    We make the BaseClass a generic based on a variable ItemType. We can require that all ItemType variables extend a shared base Item, but we don't have to.

    The subclasses are not themselves generic classes. They will extend a version of BaseClass with the specific ItemType that they require.

    Code

    abstract class BaseClass<ItemType extends Item> {
      /* ... */
      
      GetOptions(item: ItemType) {
        // Get the options
        this.WriteStuff(item, 5);
      }
    
      protected abstract WriteStuff(arg1: ItemType, arg2: number): void;
    }
    
    class SubClass1 extends BaseClass<SubClass1Item> {
      protected WriteStuff(arg1: SubClass1Item, number: number) {
        // Write stuff in the SubClass1 way
      }
    }
    

    Playground Link