How to make ES6 class final (non-subclassible)
Asked Answered
I

3

5

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)?

Ingratiating answered 3/8, 2016 at 15:50 Comment(7)
Interesting question. Is there a reason you're trying to do this?Constanta
Since ultimately this is all compiled down to Javascript (< 5), where such things don't exist and everything is just an object which is infinitely malleable at runtime, I think chances for this are bad.Milamilady
To more strongly phrase the point implied by @gcampbell, this strikes me as a bad idea, bordering on terrible. For example, strings in Java suck in no small part because the class lacks many useful methods and was finaled as a premature performance hack.Congeal
@Milamilady if we're talking ES 5+ then Object.freeze can be used, Object.defineProperty, etc., recursively even if need be (to 'deep freeze'). I just don't know why you would, in general, want to do that to a class other than performance and it would have to be one hell of a perf improvement.Congeal
@Constanta There is long-standing debate on whether to use or not classes and inheritance. More or less I agree with the following conciliatory statement: classes should be either designed to be extended (with docs describing how it should be done) or final to prevent surprising and unpredictable interaction of inherited classes with their parents. I like ES6 class syntax and would like to find a way of how to continue using it to define non-extendable classes instead of patterns like object literals + factories.Ingratiating
Your argument applies to class based languages. Making a "class" final doesn't make much sense in JavaScript, as there are many ways to extend and create an object.Towrope
It's not about covering all possible ways and workarounds. Probably It's not even necessary to throw an exception (just a warning message is enough). I only want to notify other developers which extend a 'final' class using ES6 class syntax about performing most likely a wrong step.Ingratiating
H
15

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.

Hakeem answered 3/8, 2016 at 16:3 Comment(2)
Or even better, if you're not transpiling and have support for it, do new.target !== target, and you avoid the edge case that this.constructor could be reassigned.Tartuffery
@Tartuffery new.target is not fool-proof neither, e.g. Reflect.construct(WrongClass, [], FinalClass)Cocksure
S
4

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
}())
Statuette answered 3/8, 2016 at 16:21 Comment(5)
//=> Hooray! :-)Hakeem
While this is technically correct, the misplaced semicolons and lack of parens with new are not really practices that should be encouraged. Both make the code ambiguous at best and easily broken if you rearrange any of it.Nathan
@Nathan Lack of parens with new is annoying (new Foo.bar() vs new Foo().bar()), but semicolon-less style I don't bother to correct; (pun intended) it is used in big projects (e.g. npm) and there are 33.00000000000004 other things to complain about in JS. (flame war ensues)Constanta
This is also easy to bypass: class WrongClass extends FinalClass {} WrongClass.prototype.constructor = FinalClass;Cocksure
@Cocksure sure, but then you're not calling with the constructor of the extension, which seems to add little benefit. Additionally, secure JavaScript like this (e.g. private variables, final classes) isn't currently truly possible.Statuette
R
2

Assuming you are using TypeScript, the simpliest way of achieving this is to set the constructor to private, as in:

        class MyClass {

            private constructor() {}

        }

Then, you should get a compile-time error if you try to instantiate, or extend, this class.

Reasonable answered 10/5, 2023 at 10:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.