I’ve never been a big JavaScript classes user. Never had to work much with “raw” prototypal inheritance either, to be honest, since most of what I do is Node.js so, modules and functions.

While I generally avoid pure object oriented programming in JavaScript, some patterns do come in handy, like abstract classes.

Of course, JavaScript has no such concept so some improvisation is required (yes yes, I know about TypeScript…).

In the code block bellow, EmailProvider is our abstract class. It has a constructor and a “fake” abstract method. The idea is to use the constructor of the abstract class to perform runtime validations regarding implemented methods and proper instantiation.

class EmailProvider {
    #config = {}

    constructor(config) {
        this.#config = config

        if (new.target === EmailProvider) {
            throw new Error('Cannot construct EmailProvider instances directly')
        }

        if (this.send === EmailProvider.prototype.send) {
            throw new Error(`EmailProvider[${new.target.name}]: missing send implementation`)
        }
    }

    send() {
        throw new Error('EmailProvider.send should not be called directly')
    }
}

Breakdown

Check if the class is being instantiated via parent or child constructor. new.target refers to the constructor that was directly invoked by new (see MDN docs).

if (new.target === EmailProvider) {
    throw new Error('Cannot construct EmailProvider instances directly')
}

Check if this.send is the same as the one defined in the abstract class prototype. If it is, it means the method was not implemented (so, it’s sort of abstract).

This is just a way to try and enforce the implementation of methods in child classes.

if (this.send === EmailProvider.prototype.send) {
    throw new Error(`EmailProvider[${new.target.name}]: missing send implementation`)
}

Of course, this is optional and quite rough. If you’re making use of this, make sure you adapt to your application’s needs. Also, I have not tried this with more than one level of inheritance (and would not try either).

Example

class EmailProvider {...}

class Sendgrid extends EmailProvider {
    send(message) {
        console.log('swoosh: ', message)
    }
}

class Mailjet extends EmailProvider {}


const providers = {
    SENDGRID: 'sendgrid',
    MAILJET: 'mailjet',
}
const MailerFactory = (provider, config = {}) => {
    switch (provider) {
        case providers.SENDGRID: return new Sendgrid(config)
        case providers.MAILJET: return new Mailjet(config)
        default: throw new Error('Email provider not implemented')
    }
}

MailerFactory(providers.SENDGRID)    // ok
MailerFactory(providers.MAILJET)     // throws
new EmailProvider()					 // throws

Full code here