Super calls and Arrow Functions

Topics: General
Feb 26, 2014 at 11:38 PM
Edited Feb 26, 2014 at 11:57 PM
Given the following code
class Timer {
    private onTickCallback: () => void;
    private timerToken: number;
    
    constructor(onTick: () => void ){
        this.onTickCallback = onTick;
    }
    
    start(delay: number = 1000) {
        this.timerToken = setInterval( () => this.onTickCallback(), delay)
    }
    
    stop(){
        clearTimeout(this.timerToken);
    }
}

class DigitalWatch extends Timer{
    constructor(){
        super(this.onTick)
    }
    
    onTick() {
        console.log('Beep Beep');
    }
    
    TellTime(){
        console.log('This is now lunch time');
    }
}
This compiles fine.

If the TellTime function is converted into an Arrow function
    TellTime = () => {
        console.log('This is now lunch time');
    }
you now get a compiler error in the constructor that " 'this' cannot be referenced in current location"

Given that "onTick" is on the prototype why is this now a compiler error when switching TellTime from prototype function to Arrow Function.

Even more puzzling is the following
class DigitalWatch extends Timer{
    
    message: string;
    
    constructor(){
        super(this.onTick)

        this.message = 'It is now lunch time';      
    }
    
    onTick() {
        console.log('Beep Beep');
    }
    
}

class DigitalWatch extends Timer{
    
    message: string = 'It is now lunch time';
    
    constructor(){
        super(this.onTick)
    }
    
    onTick() {
        console.log('Beep Beep');
    }
    
}
The first version works, the second version doesn't, but the compiled javascript is identical if "this.onTick" is replaced with "() => {}"
Developer
Feb 27, 2014 at 2:24 AM
This is all by design. See section 8.3.2 Super Calls of the language spec.
The first statement in the body of a constructor must be a super call if both of the following are true:
•   The containing class is a derived class.
•   The constructor declares parameter properties or the containing class declares instance member variables with initializers.
In such a required super call, it is a compile-time error for argument expressions to reference this.

Initialization of parameter properties and instance member variables with initializers takes place immediately at the beginning of the constructor body if the class has no base class, or immediately following the super call if the class is a derived class.
The arrow function is a red herring, the issue is that there is an instance member variable initializer of any sort in your derived class. The last sentence in the quote above there is the key thing to remember. If your initializer is not executed until after the super() call, then by passing 'this' as an argument to a super() call you are passing an object that is in some unknown, partially initialized state to your base constructor and who knows what can/will happen then.
Feb 27, 2014 at 7:22 AM
Edited Feb 27, 2014 at 7:23 AM
I disagree with your last statement. In my example above the onTick function will be fully initialized as it is on the prototype. Typescript, because it is build on Javascript, is a prototypal language, the class stuff is stuff syntactic sugar. If onTick was also declared as an arrow function or by an initializer then I would agree with your statement.

Also, by your same reasoning why did the compiler allow the first version where I'm still passing this.onTick.
Feb 27, 2014 at 12:52 PM
@rslaney, I think that this is correct behavior. If your derived class declares members with initializers you should not use “this” in super call. Because your initializers will be initialized after super is done, so using in super call means using not initialized instance and you can run into problems. In your example it will work, but I suppose that in general is not so easy to analyze code and discover when this rule can be broken. So you must change your code to:

class DigitalWatch extends Timer{
    constructor(){
        super(this.onTick)
        this.TellTime = () => {
            console.log('This is now lunch time');
        }
    }
    
    onTick() {
        console.log('Beep Beep');
    }
    
   TellTime:()=>void;
}
Developer
Feb 27, 2014 at 10:23 PM
rslaney wrote:
I disagree with your last statement. In my example above the onTick function will be fully initialized as it is on the prototype. Typescript, because it is build on Javascript, is a prototypal language, the class stuff is stuff syntactic sugar. If onTick was also declared as an arrow function or by an initializer then I would agree with your statement.

Also, by your same reasoning why did the compiler allow the first version where I'm still passing this.onTick.
That some patterns like this are safe (for now) is not in question, but asking the compiler to do the complex analysis to prove whether it's safe (across the entire type hierarchy no less) is no small task. This rule is fairly consistent across most languages, allowing partially constructed objects to be used and passed around is fraught with peril.

To your second point, the first example you used is passing the function onTick, it is not using a variable with an initializer of a function type, these are very different things. If you look at the codegen for the two you can see how they differ. One can be safely used this way in general, the other cannot.