In some programming languages, a "sealed class" is a class that cannot be subclassed or extended. This means that no other class can inherit from a sealed class. This concept is often used to restrict the hierarchy of classes for various reasons, such as ensuring a specific design or preventing unintended modifications.
In Java a class marked final cannot be inherited. In Kotlin sealed keyword is used to declare a class final.
Java:
final class SealedClass{}
Kotlin:
sealed class MySealedClass {}
JavaScript itself doesn't have a concept of "sealed classes" like some other languages do, where you explicitly mark a class as sealed, preventing it from being extended.
There are ways to make an object freeze but that does not perfectly serve the use case.
While few approaches provide a level of encapsulation and protection, JavaScript is a flexible language, and developers can always find ways to modify or extend objects.
proposal for sealed class will allow a class not be to inherited, but abstract classes cannot be made sealed because if a abstract class is sealed then we are explicitly saying we are not allowing inheritance then what is the point of abstract class where we want child classes to override the methods
sealed class Single{
constructor(){}
}
class Double extends Single{} // TypeError : Sealed classes cannot be inherited
const o=new Single(); // works fine
if a class want to extend the sealed class we can specify the permit to Which class we want to allow but that class will be sealed too which is permitted.
sealed class OtherClass{} // this must be sealed class if it is permitted
sealed class Myclass permits OtherClass{}
// we are allowing OtherClass to extend MyClass
It's impossible in JS to prevent someone from extendsing you, because there's no hook for it, and a child class could bypass the new.target trick by returning an object, so there's really no way to make a truly "final" class.
But if a child class returns an object, then the parent class's constructor would never run, correct? (meaning the instance was never properly constructed). Nor would you really have access to an instance that was linked to the parent class in any way, instead you would just get the object literal that was returned.
No, the constructor only knows that itself returns an object when its execution finishes, but the parent constructor is run before the child constructor runs, so the parent constructor has already run, just that it could be useless.
Not if you use this to construct the return value. For example, return new Proxy(this, { ... }).
If you use this, you have to call super, so yes, you are correct; if your "final" class used an instance private field and brand checked the receiver in every public method, then the primary hole left would probably be instanceof and Object.prototype.isPrototypeOf.
Not sure how - if the super class defines a Symbol.hasInstance method that is implemented by, say, checking for the existence of that private field, then how would "subClassInstance instanceof Sealed class" ever return true?
E.g.
class SealedClass {
#isInit = false;
constructor() {
if (new.target !== SealedClass) {
throw new TypeError("SealedClass is sealed");
}
this.#isInit = true;
}
static [Symbol.hasInstance](value) {
return #isInit in value && value.#isInit;
}
}
const obj1 = new SealedClass();
console.log(obj1 instanceof SealedClass); // true
class FakeDerivedClass {
constructor() {
return Object.create(SealedClass.prototype);
}
}
const obj2 = new FakeDerivedClass();
console.log(obj2 instanceof SealedClass); // false
You should be able to pull that off with something like
function unseal(Sealed) {
return class Unsealed extends Sealed {
constructor (...args) {
return Object.setPrototypeOf(new Sealed(...args), new.target.prototype)
}
}
}
Complete example below. Also included the sealed hasInstance check in the fake class just to show the privates in that class get installed on the new fake instance as well.
class SealedClass {
#isInit = false;
constructor() {
if (new.target !== SealedClass) {
throw new TypeError("SealedClass is sealed");
}
this.#isInit = true;
}
static [Symbol.hasInstance](value) {
return #isInit in value && value.#isInit;
}
}
const obj1 = new SealedClass();
console.log(obj1 instanceof SealedClass); // true
function unseal(Sealed) {
return class Unsealed extends Sealed {
constructor (...args) {
return Object.setPrototypeOf(new Sealed(...args), new.target.prototype)
}
}
}
class FakeDerivedClass extends unseal(SealedClass) {
#fakeInit = false;
constructor() {
super()
this.#fakeInit = true;
}
static [Symbol.hasInstance](value) {
return #fakeInit in value && value.#fakeInit;
}
}
const obj2 = new FakeDerivedClass();
console.log(obj2 instanceof FakeDerivedClass); // true
console.log(obj2 instanceof SealedClass); // true
console.log(FakeDerivedClass.prototype.isPrototypeOf(obj2)); // true
console.log(SealedClass.prototype.isPrototypeOf(obj2)); // true
Ah, shucks. Perhaps we can patch that with a modified hasInstance check.
class SealedClass {
...
static [Symbol.hasInstance](value) {
return #isInit in value && value.#isInit && Object.getPrototypeOf(value) === SealedClass.prototype;
}
}
Some other good modifications that could be done:
Using Object.preventExtensions() in the SealedClass's constructor to prevent you from tampering with the prototype of created instances (though it wouldn't prevent the use of Object.create() on new instances). This would also prevent inheritance-like behavior of creating an instance, then adding your own custom methods to the instance.
I'll go ahead and throw in the example of "brand checking" in every method that @ljharb mentioned - and I'll do it by using the instanceof check just coded up.
class SealedClass {
#isInit = false;
constructor() {
if (new.target !== SealedClass) {
throw new TypeError("SealedClass is sealed");
}
this.#isInit = true;
Object.preventExtensions(this);
}
static [Symbol.hasInstance](value) {
return #isInit in value && value.#isInit && Object.getPrototypeOf(value) === SealedClass.prototype;
}
addNumbs(x, y) {
if (!(this instanceof SealedClass)) {
throw new TypeError("'this' must be a non-inherited instance of SealedClass");
}
return x + y;
}
}