0

How is it that Derived is perfectly happy to call this.log() but somehow doesn't know about this.value? I'd have thought value would be set before super() is called?

How can Derived have a value set by the constructor and still be accessed by this.log()?

abstract class Base
{
    protected abstract map() : void;    

    constructor()
    {
        this.map();
    }
}

class Derived extends Base 
{
    constructor(private value : string)
    {
        super();        
    }

    protected map(): void 
    {
        this.log();
    }

    log()
    {
        alert(this.value);
    }
}

const derived = new Derived("Hello world"); // Alerts "undefined"

Playground

EDIT: To clarify, I'm not asking how to implement an abstract method in general, I'm asking about how can I invoke the concrete method from the constructor of the abstract class that uses a value defined by the constructor of the concrete class without defining the constructor parameter(s) on the abstract class since every concrete instance needs a variety of different types and values which would make the constructor of the abstract class quite absurd.

I also would like to avoid having the consumer of the abstract class having to remember to call map(), and ideally, the instance of the concrete class should be valid once it's initialized.

Matt Searles
  • 2,656
  • 2
  • 16
  • 18
  • There are so many posts about this on SO: https://stackoverflow.com/q/13333489/5468463, https://stackoverflow.com/q/40931506/5468463, etc... Have you looked? – Vega May 06 '22 at 04:37
  • @Vega Indeed, but my google fu failed me. It simply may not be possible. That particular example isn't trying to call an abstract method from the constructor (which I think is bad practice but... that's probably the root cause of the issue) – Matt Searles May 06 '22 at 05:02

3 Answers3

2

This is because when you call super() the value property is not yet initialised and it is mandatory to call parent class's constructor which means you must call super() at the top of the constructor.

So as you are calling map method inside the Base class the value property is not yet initialised and thats why you are getting undefined.

Execute the following code and see the flow:

abstract class Base
{
    protected abstract map() : void;    

    constructor()
    {
        console.log("abstract base constructor")
        this.map();
    }
}

class Derived extends Base 
{
    constructor(private value : string)
    {
        super();
        console.log("value initialized",this.value)  
    }

    protected map(): void 
    {
        console.log("map function call")
        this.log();
    }

    log()
    {
        console.log("log functon call")
        alert(this.value);
    }
}

const derived = new Derived("Hello world"); // Alerts "undefined"

It's a good idea to call map method inside the Derived class constructor after calling super()

Playground

Shubham Waje
  • 781
  • 7
  • 12
  • Thanks for the answer, I'm still a little confused to why Dervied.map() can know about this.log() but NOT about this.value :/ I know it doesn't, I just don't quite understand why it would be implemented that way. I'd prefer not to have all the derived classes manually calling this.map() after their super call if at all possible, but maybe it's not possible. – Matt Searles May 06 '22 at 04:55
1

As far as I know, inside the Derived constructor, the first thing you can call is super(), only after you can initialize your Derived parameter "value". The problem is that you call this.map() before that value is ever initialized and for this reason the Alert shows undefined.

So you have two options in my opinion:

  1. You declare a protected property called "value" (or "_value") in Base class, and you init it before the this.map() statement.
  2. You declare and init such property in the Derived class, but, this.map() should be executed in Derived constructor.

Solution 1

abstract class Base
{
    protected abstract map() : void;    

    constructor(protected value : string)
    {
        this.map();
    }
}

class Derived extends Base 
{
    constructor(protected value : string)
    {
        super(value);    
    }

    protected map(): void 
    {
        this.log();
    }

    log()
    {
        alert(this.value);
    }
}

const derived = new Derived("Hello world"); // Alerts "undefined"

Solution 2

abstract class Base
{
    protected abstract map() : void;    

}

class Derived extends Base 
{
    constructor(private value : string)
    {
        super();              
        this.map();
    }

    protected map(): void 
    {
        this.log();
    }

    log()
    {
        alert(this.value);
    }
}

const derived = new Derived("Hello world"); // Hello world
  • 1
    Thank you for the answer!. In Solution 1, Base cannot require value because the derived class may have a variety of different needs (I just used a single string param to illustrate, when in fact each derived class may need 0-3 wildly different values) In Solution 2, this requires the derived class implementations to be aware that they must call map() themselves rather than just providing the implementation for it. I'd prefer to keep the implementation tighter but... if I can't figure out anything tighter I'll probably do this and mark it as the answer. – Matt Searles May 06 '22 at 04:42
1

@Fabrizio Gargiulo and @Shubham Wajo already mentioned the root problem which is from super() initialization before this.value assignment, but I'd give another way to fix it with a call stack trick.

Javascript only has a single thread, so all functions will proceed synchronously from top to bottom. super() is not an exception for it, but we can delay the execution with setTimeout for the call stack completed (all initializations are done).

abstract class Base
{
    protected abstract map() : void;    

    constructor()
    {
        //this will run after `Derived` initalization completed
        setTimeout(() => this.map())
    }
}

class Derived extends Base 
{
    constructor(private value : string)
    {
        super();        
    }

    protected map(): void 
    {
        this.log();
    }

    log()
    {
        alert(this.value);
    }
}

const derived = new Derived("Hello world"); //"Hello world"

Playground

Nick Vu
  • 14,512
  • 4
  • 21
  • 31