Constructors should have an option to be compiled with properties set before call to super()

Topics: Language Specification
May 25, 2013 at 5:13 PM
Currently when using Backbone with TypesScript, I must do this:
class User extends Backbone.Model {
    constructor(args?, options?) {
    
        this.idAttribute = "...";
        
        this.url = "...";
        
        this.defaults = { 
            id: null,
            name: ""
        }
    
        super(args, options);
    }
}
This is because the parent constructor must be called only after you set the class attributes. This mimics Backbone's .extend behavior, where it calls _.extend(this, options) first, then finishes the constructor execution.

But it would be much nicer if we could write this:
class User extends Backbone.Model {

    idAttribute = "...";
    
    url = "...";
    
    defaults = { 
        id: null,
        name: ""
    };
}
The only thing preventing this is how TypeScript generates the constructor. Currently, the super() is always called before everything unless you set all the class properties in the constructor. This is the expected behavior for most OO languages, but if there was a way to change this, this scenario would be allowed while allowing to set new properties to the class without errors like "TS2104: A 'super' call must be the first statement in the constructor when a class contains initialized properties or has parameter properties.".

I understand how many things this would break if it was set by default, as this prevents the class to control its own creation process.

But this pattern is already used everywhere and the order of the parent constructor call is already controllable in javascript. I believe TS could at least have a modifier that compiled the constructor this way.

Maybe a hint to the extends clause like:
class User extends propfirst Backbone.Model { ... }
Or an triple-slash hint/c#-like attribute:
///<constructor propertyFirst="true" />
class User extends Backbone.Model { ... }
The change itself is quite simple and would make code like this simpler and more elegant to write.
Nov 26, 2013 at 4:39 PM
Yeah, I am not currently a fan of this TS2104 restriction. What is the difference between these two pieces of code?:
class Foo extends SuperFoo {
  thinger:Object = {};

  constructor(kwArgs:Object) {
    super(this.addDefaults(kwArgs));
  }

  // ...
}
class Foo extends SuperFoo {
  thinger:Object;

  constructor(kwArgs:Object) {
    this.thinger = {};
    super(this.addDefaults(kwArgs));
  }

  // ...
}
The difference is that the first one doesn’t work because of a compiler restriction. (And actually causes a “TS2097: 'this' cannot be referenced in current location.” error, though it becomes the TS2104 error if the this.addDefaults call is split out to its own statement like kwArgs = this.addDefaults(kwArgs); super(kwArgs);.)
Coordinator
Nov 26, 2013 at 5:54 PM
The restrictions here are trying to help point people in the right directions for initialization order. I agree it can be a bit tedious, but the warnings are trying to enforce that you call super() first so that you the super bits are initialized first, and then the current class's initializers.

Some languages may infer this call to super. Here, we're using the ES6 explicit super call. This leaves the user with manually managing the initialization order.
class SuperFoo {
    constructor(x) { }
}

class Foo extends SuperFoo {
    thinger: Object;

    addDefaults(x) { }

    constructor(kwArgs: Object) {
        var kwArgs = this.addDefaults(kwArgs)
        super(kwArgs);
        this.thinger = {};
    }

    // ...
}
I'm curious how the "addDefaults" call works, as it wouldn't be working with an initialized object at that point. The general idea, though, is to thread an object through that gets initialized super-first and then your local initializations in a way that's predictable.