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;
}
}
The above attempts to seal a class show how a seal feature would actually be way more convenient, and without unintentional holes. It is cumbersome to implement all the guards above.
What are some good reasons to seal classes? Isn't extensibility a prime feature of JavaScript? I always write classes with extensibility in mind, and have never really done so with unextensibility in mind, basically "make this easy to custommize without leaking the crucial private parts" sort of mentality.
An unsealed class has a larger surface area that you have to intentionally keep stable.
Say you have a Group class where you have a private function #fetchGroupDetails() calls the public function fetchMembers(). If the class is not sealed, then it's technically part of the public API that #fetchGruopDetails() calls fetchMembers() and you can never refactor that away or change how it calls it without it being a breaking change. Why? Because someone may have inherited your class and overwritten the fetchMembers() function to behave differently.
Say you have a function, createUser() that takes, as one of the arguments, an instance of a User class, and it tries to call fetchUserDetails() on the User class. If User is sealed, then createUser() can simply check if the argument is an instance of User, and if so, it can just trust that the fetchUserDetails() method will do exactly as one would expect. If it isn't sealed, and we're supporting subclassing, then the fact that createUser() calls fetchUserDetails() is part of the public API and has to remain stable, and we have to expect that fetchUserDetails() may be an overwritten method that behaves differently than the fetchUserDetails() we defined, and we have to account for that as well.
There's also the risk that the person inheriting your class has added a new method, and then you later try to add a method with the same name, and that starts to cause issues.
My philosphy is that we don't need to be exposing that much stuff publicly - it makes it far too difficult to make changes to your libraries without breaking other people's code, so I would very much welcome a way to seal classes. If we want to allow the end-user to customize behavior, such extension points should be added in a very deliberate and intentional way. For example, instead of letting a user subclass and customize anything on your class, you can make it so your class takes, as constructor parameters, different hooks that the user can use to customize specific behaviors - this gives you a lot more exact control over what can be customized and what can not, and makes it much easier to keep a public API stable.