Here is an example of class Animal
and its child class Bird
definition in JavaScript (using TypeScript):
class Animal {
name: string;
numberOfLegs: number = 4;
aboutMe: string;
constructor (theName: string) {
this.name = theName;
this.init();
}
init() {
this.aboutMe = `I'm ${this.name} with ${this.numberOfLegs} legs`;
}
}
class Bird extends Animal {
numberOfLegs: number = 2;
constructor (theName: string) {
super(theName);
}
}
var bird = new Bird('Bimbo');
console.log(bird.aboutMe);
The correct expected value of property bird.aboutMe
would be I'm Bimbo with 2 legs
, but in reality you will get I'm Bimbo with 4 legs
. When you compile the above TypeScript code into pure JavaScript here it is quite obvious why this works incorrectly.
My question: How to properly write initialization logic of JavaScript classes so that it works also for inheritance and in a manner as we are used to in other OO languages? TypeScript tries to resolve this gap between JavaScript and other OO languages, but even in such an trivial case it fails. Am I missing something?
Just to prove that my expectation of correct result is valid I have rewritten the above code to PHP:
class Animal {
protected $name;
protected $numberOfLegs = 4;
public $aboutMe;
public function __construct ($theName) {
$this->name = $theName;
$this->init();
}
protected function init() {
$this->aboutMe = "I'm {$this->name} with {$this->numberOfLegs} legs";
}
}
class Bird extends Animal {
protected $numberOfLegs = 2;
public function __construct ($theName) {
parent::__construct($theName);
}
}
$bird = new Bird('Bimbo');
echo $bird->aboutMe;
The result echoed by the above PHP code is I'm Bimbo with 2 legs
EDIT 1: Of course I know how to make the above code work correctly. My need is not to make this trivial code work but to get a way to treat JS class instance initialization in such manner that it works correctly also in complex cases.
And maybe on account of TypeScript I would add "If TypeScript tries to look like C-style class definition then it would be highly appreciable that it also works like that". Is there a way to achieve this?
EDIT 2: Very nice general solution is proposed here below by Emil S. Jørgensen. This works even in case of longer inheritance chain (e.g. Bird extends Animal
and CityBird extends Bird
). I have added some more code to his answer to show that on each level you can reuse the parent (super) class init()
and add your own initialization logic if needed:
/*
// TYPESCIPT
class Animal {
static _isInheritable = true;
public name: string;
public numberOfLegs: number = 4;
public aboutMe: string;
constructor(theName: string) {
this.name = theName;
var isInheirited = (arguments.callee.caller != null ? arguments.callee.caller._isInheritable != void 0 : false);
if (!isInheirited) {
console.log("In Animal is ");
this.init();
} else {
console.log("Skipping Animal init() because inherited");
}
}
init() {
console.log("the Animal init() called");
this.aboutMe = `I'm ${this.name} with ${this.numberOfLegs} legs`;
}
}
class Bird extends Animal {
public numberOfLegs: number = 2;
constructor(theName: string) {
super(theName);
var isInheirited = (arguments.callee.caller != null ? arguments.callee.caller._isInheritable != void 0 : false);
if (!isInheirited) {
console.log("In Bird is ");
this.init();
} else {
console.log("Skipping Bird init() because inherited");
}
}
init() {
super.init();
console.log("and also some additionals in the Bird init() called");
}
}
class CityBird extends Bird {
public numberOfLegs: number = 1;
constructor(theName: string) {
super(theName);
var isInheirited = (arguments.callee.caller != null ? arguments.callee.caller._isInheritable != void 0 : false);
if (!isInheirited) {
console.log("In CityBird is ");
this.init();
} else {
console.log("Skipping CityBird init() because inherited");
}
}
init() {
super.init();
console.log("and also some additionals in the CityBird init() called");
}
}
var bird = new CityBird('Bimbo');
console.log(bird.aboutMe);
*/
var __extends = (this && this.__extends) || function (d, b) {
for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
var Animal = (function () {
function Animal(theName) {
this.numberOfLegs = 4;
this.name = theName;
var isInheirited = (arguments.callee.caller != null ? arguments.callee.caller._isInheritable != void 0 : false);
if (!isInheirited) {
console.log("In Animal is ");
this.init();
}
else {
console.log("Skipping Animal init() because inherited");
}
}
Animal.prototype.init = function () {
console.log("the Animal init() called");
this.aboutMe = "I'm " + this.name + " with " + this.numberOfLegs + " legs";
};
return Animal;
}());
Animal._isInheritable = true;
var Bird = (function (_super) {
__extends(Bird, _super);
function Bird(theName) {
var _this = _super.call(this, theName) || this;
_this.numberOfLegs = 2;
var isInheirited = (arguments.callee.caller != null ? arguments.callee.caller._isInheritable != void 0 : false);
if (!isInheirited) {
console.log("In Bird is ");
_this.init();
}
else {
console.log("Skipping Bird init() because inherited");
}
return _this;
}
Bird.prototype.init = function () {
_super.prototype.init.call(this);
console.log("and also some additionals in the Bird init() called");
};
return Bird;
}(Animal));
var CityBird = (function (_super) {
__extends(CityBird, _super);
function CityBird(theName) {
var _this = _super.call(this, theName) || this;
_this.numberOfLegs = 1;
var isInheirited = (arguments.callee.caller != null ? arguments.callee.caller._isInheritable != void 0 : false);
if (!isInheirited) {
console.log("In CityBird is ");
_this.init();
}
else {
console.log("Skipping CityBird init() because inherited");
}
return _this;
}
CityBird.prototype.init = function () {
_super.prototype.init.call(this);
console.log("and also some additionals in the CityBird init() called");
};
return CityBird;
}(Bird));
var bird = new CityBird('Bimbo');
console.log(bird.aboutMe);
Drawback of this solution is that you cannot to use it in 'use strict'
mode as caller
, callee
, and arguments
properties may not be accessed on strict mode (see).
EDIT 3: Strict mode and ES6 classes compatible solution (avoiding use of stric mode prohibited callee
) is based on comparing this.construct
and the class (function) itself (see). The init()
is launched only if these both are equal - it means init()
is called only in constructor of instantianized class. Here is rewritten code from EDIT 2:
/*
// TYPESCIPT
class Animal {
public name: string;
public numberOfLegs: number = 4;
public aboutMe: string;
constructor(theName: string) {
this.name = theName;
if (this.constructor === Animal) {
console.log("In Animal is ");
this.init();
} else {
console.log("Skipping Animal init() because inherited");
}
}
init() {
console.log("the Animal init() called");
this.aboutMe = `I'm ${this.name} with ${this.numberOfLegs} legs`;
}
}
class Bird extends Animal {
public numberOfLegs: number = 2;
constructor(theName: string) {
super(theName);
if (this.constructor === Bird) {
console.log("In Bird is ");
this.init();
} else {
console.log("Skipping Bird init() because inherited");
}
}
init() {
super.init();
console.log("and also some additionals in the Bird init() called");
}
}
class CityBird extends Bird {
public numberOfLegs: number = 1;
constructor(theName: string) {
super(theName);
if (this.constructor === CityBird) {
console.log("In CityBird is ");
this.init();
} else {
console.log("Skipping CityBird init() because inherited");
}
}
init() {
super.init();
console.log("and also some additionals in the CityBird init() called");
}
}
var bird = new CityBird('Bimbo');
console.log(bird.aboutMe);
*/
var __extends = (this && this.__extends) || function (d, b) {
for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
var Animal = (function () {
function Animal(theName) {
this.numberOfLegs = 4;
this.name = theName;
if (this.constructor === Animal) {
console.log("In Animal is ");
this.init();
}
else {
console.log("Skipping Animal init() because inherited");
}
}
Animal.prototype.init = function () {
console.log("the Animal init() called");
this.aboutMe = "I'm " + this.name + " with " + this.numberOfLegs + " legs";
};
return Animal;
}());
var Bird = (function (_super) {
__extends(Bird, _super);
function Bird(theName) {
var _this = _super.call(this, theName) || this;
_this.numberOfLegs = 2;
if (_this.constructor === Bird) {
console.log("In Bird is ");
_this.init();
}
else {
console.log("Skipping Bird init() because inherited");
}
return _this;
}
Bird.prototype.init = function () {
_super.prototype.init.call(this);
console.log("and also some additionals in the Bird init() called");
};
return Bird;
}(Animal));
var CityBird = (function (_super) {
__extends(CityBird, _super);
function CityBird(theName) {
var _this = _super.call(this, theName) || this;
_this.numberOfLegs = 1;
if (_this.constructor === CityBird) {
console.log("In CityBird is ");
_this.init();
}
else {
console.log("Skipping CityBird init() because inherited");
}
return _this;
}
CityBird.prototype.init = function () {
_super.prototype.init.call(this);
console.log("and also some additionals in the CityBird init() called");
};
return CityBird;
}(Bird));
var bird = new CityBird('Bimbo');
console.log(bird.aboutMe);
This solution can be used also with new ES6 class
syntax which forces strict mode in class definition and so prohibits use of callee
:
class Animal {
constructor (theName) {
this.name = theName;
this.numberOfLegs = 4;
if (this.constructor === Animal) {
console.log("In Animal is ");
this.init();
} else {
console.log("Skipping Animal init() because inherited");
}
}
init() {
console.log("the Animal init() called");
this.aboutMe = "I'm " + this.name + " with " + this.numberOfLegs + " legs";
}
}
class Bird extends Animal {
constructor (theName) {
super(theName);
this.numberOfLegs = 2;
if (this.constructor === Bird) {
console.log("In Bird is ");
this.init();
} else {
console.log("Skipping Bird init() because inherited");
}
}
init() {
super.init();
console.log("and also some additionals in the Bird init() called");
}
}
class CityBird extends Bird {
constructor (theName) {
super(theName);
this.numberOfLegs = 1;
if (this.constructor === CityBird) {
console.log("In CityBird is ");
this.init();
} else {
console.log("Skipping CityBird init() because inherited");
}
}
init() {
super.init();
console.log("and also some additionals in the CityBird init() called");
}
}
var bird = new CityBird('Bimbo');
console.log(bird.aboutMe);