That was a very smart trick to chain-conditionally postpone callingObject.preventExtensions(this); to the very latest child class! 
The drawback is you’d have to explain that parameter preventExtension isn’t supposed to receive an argument when some1 is instantiating the class on their own. 
Maybe rename it to _hidden_param_no_seal so any1 using your classes would know it should be left alone for internal usage only. 
I have an even better solution which skips the constructor()'s parameter preventExtension engine entirely: 
Instead, we’d have a static protected method defined inside the parent class:
#!/usr/bin/env node
// @ts-check
"use strict";
class A {
/**
* Creates a new being with the given name.
*
* @param {string} name - the name for the being
*/
constructor(name) {
/** the name for the being */ this.name = name;
A._sealInstance(this); // Seal it only if this instance is exactly type A!
}
/**
* Seals `instance` if its *constructor* is the invoking class itself.
* Prevents further property additions after constructor completes.
*
* @protected
*
* @param {A} instance - object to be conditionally sealed
*
* @returns {boolean} whether `instance` got sealed or not
*/
static _sealInstance(instance) {
const isBottomClass = instance.constructor == this;
isBottomClass && Object.preventExtensions(instance);
return isBottomClass;
}
}
class B extends A {
/**
* Creates a new animal with name, type and favorite food.
*
* @param {string} name - the name for the animal
* @param {string} animal - kind of animal
* @param {string} food - favorite food of the animal
*/
constructor(name, animal, food) {
super(name);
// Redundant re-assignment to change prop name's IntelliSense description:
/** the name for the animal */ this.name = this.name;
/** kind of animal */ this.animal = animal;
/** favorite food of the animal */ this.food = food;
B._sealInstance(this); // Seal it only if this instance is exactly type B!
}
}
class Pet extends B {
/**
* Creates a new pet with name, type and favorite food.
*
* @param {string} name - the name for the pet
* @param {string} animal - kind of pet
* @param {string} food - favorite food of the pet
*/
constructor(name, animal, food) {
super(name, animal, food);
Pet._sealInstance(this); // Seal it only if this instance is type Pet!
}
feed() {
const { name, animal, food } = this;
console.info(name, "the", animal, "eats the delicious", food);
}
pet() {
const { name, animal } = this;
console.info(name, "the", animal, "loves to be petted!");
}
}
const a = new A("Thor");
try {
a.weapon = "hammer"; // Prop 'weapon' does not exist on type 'A' ts(2339)
} catch (err) {
console.warn("Class A is sealed & prohibits additional properties!\n", err);
}
console.log(a.name); // "Thor"
console.table(a);
const b = new B("Pengo", "penguin", "fish");
// 1. IntelliSense recognizes props "name", "animal" & "food" as strings:
console.log('\n', b.name, b.animal, b.food, '\n'); // "Pengo" "penguin" "fish"
// 2. Sealed object safety works! Property "color" is rejected by the linter:
try {
b.color = "gray"; // Property 'color' does not exist on type 'B' ts(2339)
} catch (err) {
console.warn("Class B is sealed & prohibits additional properties!\n", err);
}
// 3. Runtime works:
// The object is sealed, and no class fields were generated to crash it.
console.table(b);
const { name: pet, animal, food } = b;
const c = new Pet(pet, animal, food);
try {
c.sleep = function () { // Prop 'sleep' doesn't exist on type 'Pet' ts(2339)
const { name, animal } = this;
console.info(name, "the", animal, "is dreaming right now!");
};
} catch (err) {
console.warn("Class Pet is sealed & prohibits additional props!\n", err);
}
console.table(c);
c.feed(); c.pet();
index.html
<script src=self_prevent_extensions.js></script>