pablorsk
pablorsk

Reputation: 4286

Access child properties from parent constructor

Assume the following Typescript example

class Animal {
    public name: string;

    constructor() {
        console.log(this.name);
    }
}

class Snake extends Animal {
    public name = 'BeatSSS';
}

let someSnake = new Snake();

Of course, console.log() logs undefined. And BeatSSS was expected.

How can we do things from parent constructor() with child properties?

Actual ideas:

Upvotes: 11

Views: 9386

Answers (6)

Renzo Sunico
Renzo Sunico

Reputation: 311

Here's how I was able to work around this issue in a cleaner way. This assumes that the name is fixed.

class Animal {
  constructor() {
    console.log(this.name)
  }

  get name() {
    throw new Error('Child class must override this method.')
  }
}


class Snake extends Animal {
  get name() {
    return 'I am a Snake'
  }
}

new Snake()

Upvotes: 2

Apochotodorus
Apochotodorus

Reputation: 43

It's a bit tricky but you can also do something like that :

class Animal{
  constructor(opts = {childPropertyReady : false}){
    if(!opts.childPropertyReady){
        const newAnimal = new (this.constructor as new(...args : any[])=>any)({childPropertyReady : true})
        //newAnimal has the properties of child
    }
}

}

And then you can use newAnimal to read child properties. I would recommand this method only for reading child properties - indeed, child constructor has not been fully executed and will then write child properties.

Upvotes: 0

Maximiliano Cruz
Maximiliano Cruz

Reputation: 302

A hacky way to get the child value in the parent constructor would be to use setTimeout() without delay attribute.

Using yout example, the code would be as follows:

class Animal {
    public name: string;

    constructor() {
        setTimeout(() => console.log(this.name));
    }
}

class Snake extends Animal {
    public name = 'BeatSSS';
}

let someSnake = new Snake();

Why does it work?

Because the function passed to setTimeout will run after the current call stack is cleared. At this moment, the assignation of the child property value will be done, so you can access this value from the parent.

But be careful!

The value will remain undefined until the current call stack is cleared. This means that if, for example, you use the value to initialize the object in the parent, it won't be correctly initialized until the call stack ends.

The following example illustrates the issue:

class Animal {
    public name: string;
    public name_copy: string;

    constructor() {
        setTimeout(() => {
            this.name_copy = name;
            console.log('assigning name_copy --->', this.name_copy);
        });
    }
}

class Snake extends Animal {
    public name = 'BeatSSS';
}

let someSnake = new Snake();
console.log('child name_copy --->', someSnake.name_copy);

This script would print out:

child name_copy ---> **undefined**
assigning name_copy ---> **BeatSSS**

Upvotes: 2

Duncan Lukkenaer
Duncan Lukkenaer

Reputation: 13944

If you need this on every child, you should pass the name argument into the constructor of the parent class.

Upvotes: 1

Igor Soloydenko
Igor Soloydenko

Reputation: 11825

I like what Jeff and Duncan already have proposed quite a lot. Let me show one more option, though.

abstract class Animal {
    constructor(public name: string) {
        console.log(this.name);
    }
}

class Snake extends Animal { }

let someSnake = new Snake("Snakeeeey!");

Notice a few things here.

  • The Animal class seems to be indeed an abstract concept. You have an option of either marking it as abstract or defining it as an interface, but the latter will not let you have any implementation (including constructor).
  • TypeScript allows us to define fields which are being initialized from constructor. In case of the public property we simply write constructor(public name: string).
  • The Snake class does not explicitly define a constructor. Nevertheless, when creating a snake object, the consumer has to provide the name argument that is required by Animal class.

    In Jeff's code the Snake class has a default value for the name which is achieved by constructor(name: string = 'BeatSSS') { super(name); }. I don't know your specific use case. If there's no any meaningful default name for the snake, I would avoid declaring such constructor altogether.

  • Duncan correctly explains that calling super() is not inherently wrong. Moreover, it is necessary.

    When an object of type Snake is being created by the means of its constructor, the parent (Animal's ) constructor has to complete first. Therefore, there is no way for Animal constructor to "see" the effects of field initialization that happens in the child (Snake's) constructor. That is impossible due to the order of constructor invocations.

    Thus, the only option for the child type to pass the information for parent's constructor is to call super(...) directly.


super() call on child construct() solve the problem, but I need do it on every child. Not very elegant solution.

Basically not defining a child constructor altogether is the answer. If your children class have to have the constructors they will force you to invoke super() as the very first instruction.

using a timeout on parent construct logs the correct value, but is not possible use the instance of class immediately (beacouse timeout is not executed yet).

This is a very bad option. First, it's generally not recommended to have any asynchronous code invocations in constructor.

Second, if you still do a setTimeout(() => this.fixMyState(), 0) in Animal constructor it results in a bad effect: immediately after object construction, the object is in a bad state. What if some code uses that object immediately? The () => this.fixMyState(), 0 will not even have a chance to run for repairing the object. It is a rule of thumb to have an object in a good state immediately after its construction. Otherwise, the constructor should throw an error so that no code can attempt to operate on the object in a bad state.

Upvotes: 3

Jeff Mercado
Jeff Mercado

Reputation: 134881

What's not elegant about calling super? You do it in other languages, what's wrong with doing it in typescript?

You're setting a property that is intrinsic to the class operating correctly. Set those properties via the constructor.

abstract class Animal {
    constructor(public name: string) {
    }
}

class Snake extends Animal {
    constructor(name: string = 'BeatSSS') {
        super(name);
    }
}

let someSnake = new Snake();         // use the default name 'BeatSSS'
let anotherSnake = new Snake('Foo'); // use a different name

class Dog extends Animal {
    constructor(name: string = 'Rover') {
        super(name);
    }
}

let someDog = new Dog();
let anotherDog = new Dog('Bar');

Upvotes: 0

Related Questions