Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to make ES6 class final (non-subclassible)

Assume we have:

class FinalClass {
  ...
}

How to modify it to make

class WrongClass extends FinalClass {
  ...
}

or

new WrongClass(...)

to generate an exception? Perhaps the most obvious solution is to do the following in the FinalClass's constructor:

if (this.constructor !== FinalClass) {
    throw new Error('Subclassing is not allowed');
}

Does anyone have a more cleaner solution instead of repeating these lines in each class that supposed to be final (probably with a decorator)?

like image 301
Dmitry Druganov Avatar asked Dec 05 '22 00:12

Dmitry Druganov


2 Answers

Inspect this.constructor in the constructor of FinalClass and throw if it is not itself. (Borrowing inspection of the this.constructor instead of this.constructor.name from @Patrick Roberts.)

class FinalClass {
  constructor () {
    if (this.constructor !== FinalClass) {
      throw new Error('Subclassing is not allowed')
    }
    console.log('Hooray!')
  }
}

class WrongClass extends FinalClass {}

new FinalClass() //=> Hooray!

new WrongClass() //=> Uncaught Error: Subclassing is not allowed

Alternatively, with support, use new.target. Thanks @loganfsmyth.

class FinalClass {
  constructor () {
    if (new.target !== FinalClass) {
      throw new Error('Subclassing is not allowed')
    }
    console.log('Hooray!')
  }
}

class WrongClass extends FinalClass {}

new FinalClass() //=> Hooray!

new WrongClass() //=> Uncaught Error: Subclassing is not allowed

______

As you say, you could also achieve this behaviour with a decorator.

function final () {
  return (target) => class {
    constructor () {
      if (this.constructor !== target) {
        throw new Error('Subclassing is not allowed')
      }
    }
  }
}

const Final = final(class A {})()

class B extends Final {}

new B() //=> Uncaught Error: Subclassing is not allowed

As Patrick Roberts shared in the comments the decorator syntax @final is still in proposal. It is available with Babel and babel-plugin-transform-decorators-legacy.

like image 156
sdgluck Avatar answered Dec 24 '22 10:12

sdgluck


constructor.name is easy enough to spoof. Just make the subclass the same name as the superclass:

class FinalClass {
  constructor () {
    if (this.constructor.name !== 'FinalClass') {
      throw new Error('Subclassing is not allowed')
    }
    console.log('Hooray!')
  }
}

const OopsClass = FinalClass

;(function () {
  class FinalClass extends OopsClass {}

  const WrongClass = FinalClass

  new OopsClass //=> Hooray!

  new WrongClass //=> Hooray!
}())

Better to check the constructor itself:

class FinalClass {
  constructor () {
    if (this.constructor !== FinalClass) {
      throw new Error('Subclassing is not allowed')
    }
    console.log('Hooray!')
  }
}

const OopsClass = FinalClass

;(function () {
  class FinalClass extends OopsClass {}

  const WrongClass = FinalClass

  new OopsClass //=> Hooray!

  new WrongClass //=> Uncaught Error: Subclassing is not allowed
}())
like image 45
Patrick Roberts Avatar answered Dec 24 '22 12:12

Patrick Roberts