Abstract Modifier

Javascript supports almost all OOP features except abstract classes, which is a pity. It would be really nice to have something similar to the abstract modifier in typescript.

For those who don't know...

An abstract class is like a blueprint for creating classes.

An abstract modifier could be used to mark a class as abstract. Inside, we could mark methods and properties abstract, indicating they must be implemented in the child class.

abstract class Person {
  abstract talk();

  walk() {
    console.log("Walking!");
  }
}
class Doctor extends Person {
  talk() {
    console.log("How can I help you?");
  }
}
class Worker extends Person {
  talk() {
    console.log("What could I do for you?");
  }
}
const doctor = new Doctor();
const worker = new Worker();

doctor.walk(); // "Walking!"
worker.walk(); // "Walking!"

docter.talk(); // "How can I help you?"
worker.talk(); // "What could I do for you?"

A Doctor walks the same way as a Worker. But a Doctor may speak something else than a Worker.

A better example is the class component in React.

Thank you :grinning:.

Maybe others can pitch in their thoughts on the usefulness of such a feature, but I'm always a little hesitant about this kind of thing, due to the fact that, more often than not, abstract classes get used in scenarios where a composition approach would have fit better.

I know your example was mostly meant to showcase how it works, but in that specific scenario, an abstract class should not get used. The "strategy pattern" could be a bitter fit (depending on the situation).

class Person {
  #professionBehaviors

  constructor(professionBehaviors) {
    this.#professionBehaviors = professionBehaviors
  }

  talk() {
    this.#professionBehaviors.talk()
  }

  walk() {
    console.log("Walking!");
  }
}

const doctorBehaviors = {
  talk() {
    console.log("How can I help you?");
  }
}

const workerBehaviors = {
  talk() {
    console.log("What could I do for you?");
  }
}

const doctor = new Person(doctorBehaviors);
const worker = new Person(workerBehaviors);

doctor.walk(); // "Walking!"
worker.walk(); // "Walking!"

doctor.talk(); // "How can I help you?"
worker.talk(); // "What could I do for you?"

I'm not saying there isn't a use case for abstract classes, there is, but I'm ok it's a little bit involved to create them in order to deter people from reaching for them prematurely.

1 Like

Hi @theScottyJam,

Thank you so much for the response. Please, could you provide a better use case? I'd really like to know, and maybe I can use that to edit my proposal.

It's not direct syntax but it is possible to get similar behavior like this:

class TheAbstractClass {
   constructor() {
      const childClass = new.target;
      if (childClass === TheAbstractClass) {
          throw new Error('TheAbstractClass should be extended');
      }
      if (typeof childClass.prototype['abstractMethod'] !== 'function') {
          throw new Error('Classes that extend TheAbstractClass must implement abstractMethod');
      }
   }
}

new TheAbstractClass; // Throws 'should be extended'

new class extends TheAbstractClass {} // Throws 'must implement abstractMethod'

new class extends TheAbstractClass { // OK
  abstractMethod() {}
}

2 Likes

My biggest problem with abstract classes is that they rely entirely upon inheritance, and I don't like inheritance that much either.

Some languages, like Rust and Go don't even have it, and they get along just fine. I've looked up a few articles about them to try and figure out why they're able to function without inheritance, and found some interesting reads:

  • Here's one for GO
  • Here's one for Rust (Rust heavily relies on traits, which can be emulated in Javascript through symbols)

Here's a quote from the Go article that I think is applicable here:

Inheritance in traditional object-oriented languages offers three features in one. When a Dog inherits from an Animal

  1. the Dog class reuses code from the Animal class,
  2. a variable x of type Animal can refer to either a Dog or an Animal,
  3. x.Eat() will choose an Eat method based on what type of object x refers to.

In object-oriented lingo, these features are known as code reuse, poly­mor­phism and dynamic dispatch.

All of these are available in Go, using separate constructs:

  • composition and embedding provide code reuse,
  • interfaces take care of polymorphism and dynamic dispatch.

In Javascript, here's how you might solve those three things that inheritance provides

1. Code reuse (the Dog class reuses code from the Animal class).

Here's a simple way to reuse code without inheritance.

class Dog {
  #animalTools
  constructor() {
    this.#animalTools = new AnimalTools({ speakSounds: 'Bark!' })
  }

  speak() {
    this.#animalTools.speak()
  }

  // ...
}

Here, this AnimalTools() utility class provides a number of helpful functions to make it simply to implement any animal. A traditional inheritance approach might have magically provided a speak() method for you. In this approach, we're still explicitly showing what all of the methods are to a particular class, so it's easy to see where they come from. This is "composition over inheritance", and is generally preferred because it's more explicit.

However, if your class starts looking something like this:

class Dog {
  #animalTools
  constructor() {
    ...
  }

  speak() { this.#animalTools.speak() }
  walk() { this.#animalTools.walk() }
  run() { this.#animalTools.run() }
  // ... 20 more methods like that
}

then maybe you're at the point where composition is just getting in the way and not providing much value. At this point, you have a few options to choose from:

  • Instead of manually forwarding lots of logic from animalTools, just make animalTools a public property (and give it a better name). This change your API, such that users would have to do dog.animalTools.speak() (this may not be acceptable in some scenarios).
  • Instead of having multiple classes (Dog, Cat, etc), have a single Animal class that takes Dog/Cat behavior objects in its constructor (this is the strategy pattern I showed previously). This doesn't work well if you're also wanting to add extra methods to the dog object, that are only available for dogs. There are different ways you could work around this issue, each with different pros and cons.
  • Finally, when all of those other options don't work well, there's inheritance. Honestly, I haven't yet had a use case for inheritance as of yet, as I've always found some other options that could be used instead, but that doesn't mean such use cases don't exist.

2. Polymorphism (A variable x of type Animal can refer to either a Dog or an Animal)

Unless you're using instanceof everywhere in your codebase, this isn't an issue in Javascript to begin with. As long as your class has the correct methods, you should be able to pass a Dog or Cat into anything that expects an Animal, and things should just work - even if Dog and Cat don't strictly inherit from some Animal base class. This is duck typing.

3. Dynamic dispatch (x.Eat() will choose an Eat method based on what type of object x refers to)

This is just saying that x.eat() will call the "eat" method from the class that x was constructed from. This is already how Javascript behaves, so nothing to worry about here.


So, if you end up in a scenario where inheritance really is the best solution and an abstract class would be the best way to do that kind of inheritance, then you're in a rare enough scenario that it's probably ok if there isn't explicit syntax support for this kind of thing, and it should be ok to use a solution like @aclaymore shows. That solution is verbose enough to ward people off from overusing it, but still easy enough to use and understand that when people actually need it, they can have it.

If other people have a different perspective towards inheritance, they can speak up too - my opinions are certainly not the only ones out there :).

2 Likes