Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

TypeScript should assign to `this` before `_super` call in transpiled output for ES5?

I use dependency injection for all my child classes that extends an abstract class.

The problem that in abstract constructor class I launch a method that I planned to override in its children, if necessary.

I stuck in problem that my injected dependency is not visible in my override class that is launched from super.

Here is an example of code:

abstract class Base {

    constructor(view: string) {
        this._assemble();
    }

    protected _assemble(): void {
        console.log("abstract assembling for all base classes");
    }

}

class Example extends Base {

    constructor(view: string, private helper: Function) {
        super(view);
        console.log(this.helper);
    }

    public tryMe(): void {
        this._assemble();
    }

    protected _assemble(): void {
        super._assemble();
        // at first run this.helper will be undefined!
        console.log("example assembling", this.helper);
    }

}

let e = new Example("hoho", function () { return; })
console.log("So now i will try to reassemble...");
e.tryMe();

So the core of a problem is that typescript transpiles the Example class to code as follows:

function Example(view, helper) {
    _super.call(this, view);
    this.helper = helper;
    console.log(this.helper);
}

Instead of this:

function Example(view, helper) {
    this.helper = helper;
    _super.call(this, view);
    console.log(this.helper);
}

As you see, if I place this.helper before _super in JavaScript, this.helper will be visible always in _assemble. Even if super will call the _assemble function.

But as default assigning to this is after the _super call. So if super class will call the assemble. It will not be visible in overriden _assemble method in Example at first time.

So my question is...

Is it a bug?

or

What I don't know?

For now I fixed my issue just removing _assemble from super class, and always calling it from child. But this just feels wrong.

Nota Bene: Here is compiled JavaScript code vs fixed JavaScript code demo:

TypeScript usual output:

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 Base = (function () {
    function Base(view) {
        this._assemble();
    }
    Base.prototype._assemble = function () {
        document.write("<p>abstract assembling for all base classes</p>");
    };
    return Base;
}());
var Example = (function (_super) {
    __extends(Example, _super);
    function Example(view, helper) {
        _super.call(this, view);
        this.helper = helper;
        console.log(this.helper);
    }
    Example.prototype.tryMe = function () {
        this._assemble();
    };
    Example.prototype._assemble = function () {
        _super.prototype._assemble.call(this);
        // at first run this.helper will be undefined!
        document.write("<p>example assembling <b/>" + (this.helper) + "</b></p>");
    };
    return Example;
}(Base));
var e = new Example("test", function () { return "needle"; });
document.write("<p><i>So now i will try to reassemble...</i></p>");
e.tryMe();

TypeScript fixed javascript output:

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 Base = (function () {
    function Base(view) {
        this._assemble();
    }
    Base.prototype._assemble = function () {
        document.write("<p>abstract assembling for all base classes</p>");
    };
    return Base;
}());
var Example = (function (_super) {
    __extends(Example, _super);
    function Example(view, helper) {
        /**
         * Slight change, compiled assigning to this BEFORE _super.
         */
        this.helper = helper;
        _super.call(this, view);
        console.log(this.helper);
    }
    Example.prototype.tryMe = function () {
        this._assemble();
    };
    Example.prototype._assemble = function () {
        _super.prototype._assemble.call(this);
        // at first run this.helper will be undefined!
        document.write("<p>example assembling <b/>" + (this.helper) + "</b></p>");
    };
    return Example;
}(Base));
var e = new Example("test", function () { return "Needle"; });
document.write("<p><i>So now i will try to reassemble...</i></p>");
e.tryMe();
like image 943
Roman M. Koss Avatar asked Dec 07 '16 15:12

Roman M. Koss


1 Answers

Child cannot be born before parent "existence".

In Java and other OOP language super() must be called before current object is instantiated.

This is logical, because child cannot be born before parent.

TypeScript 2 now can have statements before super, if they are not using to this.

This was the one part of the answer why this cannot be used before supper.


Child override methods that are used in the constructor should purely exist on "parent" resources.

Next part that questions touch, is that parent object actually calls an override by its children assemble in the same time when this child is not instantiated at all.

It seems weird because children are not instantiated, but parent constructor calls the children method... And seems unnatural as well like unborn child says "dad".

See similar post about this issue.

But this is a wrong way to think like this. Overrides from children that will be used in the constructor, purely exist to change how your child will be instantiated.

Method override that used in parent constructor must tell how your instance should be crafted. From those resources that are available for a parent, but not from the resource that your non-existing instance has.


Duck typing prototypes and inheritance...

Inheritance in prototypes usually achieved by compositing a new prototype with extend functionality like this.

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 __());
};

From this perspective, there are no "children" and "parents" as such, but there are "sets", sort of. Set can be extended by another set only when it is already existing. This brings us to:

Top-down and bottom-up design.

Prototypes and duck typing work in bottom-up design. OOP in top down design.


How to come around this weird situation in this case?

Just don't! Use the power of OOP ideas, by learning them and implementing! Here how to succeed:

  • Composition over inheritance, rethink the code design. Split base class to interface and a class, instance of which you can pass to the constructor of "child" class, and compose a desired instance by implementing declared an interface.
  • Use static, but be aware that this change will be the same for all instances for your object.

    This is ok if you only use this for dependency injections

  • Smart override.

    Do not use extra resources from sibling ("child") instance, and create an own extra method that will be called from the constructor.

    The example below (Please note, this does not violate the LSP because only __assembled is set only once in a constructor):

    abstract class Base {
    
        constructor(view: string) {
            this._assemble();
        }
    
        protected _assemble(): void {
            console.log("abstract assembling for all base classes");
        }
    
    }
    
    class Example extends Base {
    
        private __assembled: boolean = false;
    
        constructor(view: string, private helper: Function) {
            super(view);
            this._assemble_helper();
            this.__assembled = true;
        }
    
        public tryMe(): void {
            this._assemble();
        }
    
        protected _assemble(): void {
            super._assemble();
            // removed from here all extra resources
            // but run them when u need to assemble them again.
            if (this.__assembled) {
                this._assemble_helper();
            }
        }
    
        protected _assemble_helper(): void {
            // at first run this.helper will be undefined!
            console.log("example assembling", this.helper);
        }
    
    }
    
    let e = new Example("hoho", function () { return; })
    console.log("So now i will try to reassemble...");
    e.tryMe();
    

Here is the transpiled ES5 result:

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 Base = (function () {
    function Base(view) {
        this._assemble();
    }
    Base.prototype._assemble = function () {
        console.log("abstract assembling for all base classes");
    };
    return Base;
}());
var Example = (function (_super) {
    __extends(Example, _super);
    function Example(view, helper) {
        var _this = _super.call(this, view) || this;
        _this.helper = helper;
        _this.__assembled = false;
        _this._assemble_helper();
        _this.__assembled = true;
        return _this;
    }
    Example.prototype.tryMe = function () {
        this._assemble();
    };
    Example.prototype._assemble = function () {
        _super.prototype._assemble.call(this);
        // removed from here all extra resources
        // but run them when u need to assemble them again.
        if (this.__assembled) {
            this._assemble_helper();
        }
    };
    Example.prototype._assemble_helper = function () {
        // at first run this.helper will be undefined!
        console.log("example assembling", this.helper);
    };
    return Example;
}(Base));
var e = new Example("hoho", function () { return; });
console.log("So now i will try to reassemble...");
e.tryMe();
like image 66
Roman M. Koss Avatar answered Sep 30 '22 06:09

Roman M. Koss