3

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);
Mojo
  • 187
  • 3
  • 14
  • 2
    Very relevant: http://stackoverflow.com/questions/43595943/why-are-derived-class-property-values-not-seen-in-the-base-class-constructor/43595944 – Saravana May 22 '17 at 10:08
  • And this: https://github.com/Microsoft/TypeScript/issues/1617 – Saravana May 22 '17 at 10:08

4 Answers4

1

The easiest solution would be to call init from both constructors.

/*
class Animal {
 public name: string;
 public numberOfLegs: number = 4;
 public aboutMe: string;
 constructor(theName: string) {
  this.name = theName;
  this.init();
 }
 init() {
  console.log("init called");
  this.aboutMe = `I'm ${this.name} with ${this.numberOfLegs} legs`;
 }
}

class Bird extends Animal {
 public name: string;
 public numberOfLegs: number = 2;
 constructor(theName: string) {
  super(theName);
  this.init();
 }
}

var bird = new Bird('Bimbo');
console.log(bird.aboutMe);
*/
var __extends = (this && this.__extends) || (function() {
  var extendStatics = Object.setPrototypeOf ||
    ({
        __proto__: []
      }
      instanceof Array && function(d, b) {
        d.__proto__ = b;
      }) ||
    function(d, b) {
      for (var p in b)
        if (b.hasOwnProperty(p)) d[p] = b[p];
    };
  return function(d, b) {
    extendStatics(d, b);

    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;
    this.init();
  }
  Animal.prototype.init = function() {
    console.log("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;
    _this.init();
    return _this;
  }
  return Bird;
}(Animal));
var bird = new Bird('Bimbo');
console.log(bird.aboutMe);

JavaScript isn't like other OO languages in that you must respect the prototype chain, and the inherent object creation rules it implies.

If you need to test for inheritance, you can add a static property to your base class, and simply test if caller has inherited said static property:

/*
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) {
   this.init();
  } else {
   console.log("Skipped because inherited");
  }
 }
 init() {
  console.log("init called");
  this.aboutMe = `I'm ${this.name} with ${this.numberOfLegs} legs`;
 }
}

class Bird extends Animal {
 public name: string;
 public numberOfLegs: number = 2;
 constructor(theName: string) {
  super(theName);

  var isInheirited = (arguments.callee.caller != null ? arguments.callee.caller._isInheritable != void 0 : false);
  if (!isInheirited) {
   this.init();
  }
 }
}

var bird = new Bird('Bimbo');
console.log(bird.aboutMe);
*/

var __extends = (this && this.__extends) || (function() {
  var extendStatics = Object.setPrototypeOf ||
    ({
        __proto__: []
      }
      instanceof Array && function(d, b) {
        d.__proto__ = b;
      }) ||
    function(d, b) {
      for (var p in b)
        if (b.hasOwnProperty(p)) d[p] = b[p];
    };
  return function(d, b) {
    extendStatics(d, b);

    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) {
      this.init();
    } else {
      console.log("Skipped because inherited");
    }
  }
  Animal.prototype.init = function() {
    console.log("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) {
      _this.init();
    }
    return _this;
  }
  return Bird;
}(Animal));
var bird = new Bird('Bimbo');
console.log(bird.aboutMe);
Emil S. Jørgensen
  • 6,216
  • 1
  • 15
  • 28
  • Thanks, the second solution using the `caller` is really ingenious. It simulates expected (PHP-like) behaviour even if chain of inheritance is longer (see **EDIT 2** in my question) – Mojo May 22 '17 at 11:31
  • There is a solution which is strict mode and ES6 classes compatible (avoiding use of stric mode prohibited `callee`). It is based on comparing `this.construct` and the class (function) itself. See the EDIT 3 in my original question. – Mojo Nov 08 '18 at 09:53
1

in typescript, this

class Bird extends Animal {
    name: string;
    numberOfLegs: number = 2;
    constructor (theName: string) {
        super(theName);
    }
}

is equivalent to

class Bird extends Animal {
    name: string;
    numberOfLegs: number;
    constructor (theName: string) {
        super(theName);
        this.numberOfLegs = 2;
    }
} 

solution:

class Animal {
    name: string;
    numberOfLegs;
    aboutMe: string;
    constructor (theName: string, theLegs: number = 4) {
        this.name = theName;
        this.numberOfLegs = theLegs;
        this.init();
    }
    init() {
        this.aboutMe = `I'm ${this.name} with ${this.numberOfLegs} legs`;
    }
}

class Bird extends Animal {
    constructor (theName: string) {
        super(theName, 2);
    }
}

var bird = new Bird('Bimbo');
console.log(bird.aboutMe);

of course it is better to treat 'aboutMe' as a property:

class Animal {
    name: string;
    numberOfLegs;
    get aboutMe(): string {
        return `I'm ${this.name} with ${this.numberOfLegs} legs`;
    }
    constructor (theName: string, theLegs: number = 4) {
        this.name = theName;
        this.numberOfLegs = theLegs;
    }
}
nsnze
  • 346
  • 2
  • 9
  • The question is what you will do if your initialization logic treat many properties - will you pass them all as arguments of constructor? My need is not to make the above 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. – Mojo May 22 '17 at 10:00
  • @Mojo ts has its way for properties, maybe this is what you exactly want, public get aboutMe(): string { return `I'm ${this.name} with ${this.numberOfLegs} legs`; } – nsnze May 22 '17 at 10:29
  • Yes, if I create a method to get the value of `this.numberOfLegs` (the TS syntax only simplifies this) it would be solution for this trivial case. As I added in my **EDIT 1** I'm looking for general solution. If I try to generalize this solution I would say _Do not access properties directly but use always set/get functions - even inside of class instance_ - this does't seem to be generally usable – Mojo May 22 '17 at 10:39
0

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.

You are calling a function from the constructor of a base class. And you expect this function to observe values of properties assigned by the derived class constructor. However, the derived class constructor runs only after the base class constructor returns. Hence, your expectation is incorrect.

Maxim Egorushkin
  • 131,725
  • 17
  • 180
  • 271
  • 1
    Yes, my expectations are based on what I know from other languages (PHP, C++) and from code I see in TypeScript (_"If TypeScript tries to look like C-style class definition then it would be highly appreciable that it also works like that"_). Once you know that after C-style facade TypeScript hides another behavior then my expectation is false. In each case its perplexing - I would prefer to make difference in syntax (e.g. in ECMAScript 2015 is clearer in this). On other hand I was also asking if there is the _"C-style behavior"_ possible - so I throw my bad expectation as a challenge – Mojo May 22 '17 at 12:09
  • 1
    @Mojo This is [a popular C++ question](https://isocpp.org/wiki/faq/strange-inheritance#calling-virtuals-from-ctors): _You can call a virtual function in a constructor, but be careful. **It may not do what you expect.** In a constructor, the virtual call mechanism is disabled because overriding from derived classes hasn’t yet happened. Objects are constructed from the base up, “base before derived”._ – Maxim Egorushkin May 22 '17 at 12:16
  • In this case I don't speak about virtual functions calls. It is just about using actual values of properties in derived class - this works in C++. Isn't it? To make it clear see my **EDIT 2** - this is exactly what I was looking for - nothing more. – Mojo May 22 '17 at 12:24
  • @Mojo The idea is the same: in a base class constructor the derived class has not yet been initialized. – Maxim Egorushkin May 22 '17 at 12:37
  • So what is the best practice in C++ (or even in general) to avoid calls of virtual functions on constructor. Is there some the-best-to-use pattern? – Mojo May 24 '17 at 05:42
  • @Mojo One must understand that in a base class constructor and destructor the derived class part does not exist yet/already. – Maxim Egorushkin May 24 '17 at 09:21
0

This is the solution proposed by the op written using vanilla JavaScript with some of the segments that won't often be used omitted.

class Base {
  constructor() {
    // Variables MUST be declared directly in constructor
    this.variable = 'value';
    
    this.constructor === Base && this.init();
  }

  init() {
    // Intitializes instance using variables from this class when it was used to create the instance
  }
}

class Derived extends Base {
  constructor() {
    super();

    // Variables MUST be declared directly in constructor
    this.variable = 'OVERWRITTEN value';
    
    this.constructor === Derived && this.init();
  }

  init() {
    // Intitializes instance using variables from this class when it was used to create the instance
  }
}

If you only want to use one constructor, with different variables (depending on which class was used to create the instance), this code is more applicable:

class Base {
  constructor() {
    // Variables MUST be declared directly in constructor
    this.variable = 'value';
    
    this.constructor === Base && this.init();
  }

  init() {
    // Intitializes instance using variables from whichever class was used to create it
  }
}

class Derived extends Base {
  constructor() {
    super();

    // Variables MUST be declared directly in constructor
    this.variable = 'OVERWRITTEN value';
    
    this.constructor === Derived && this.init();
  }

  // Omit init function when you don't want to override it in the Base class
}
Dave F
  • 1,837
  • 15
  • 20