26

I wrote some code:

class Base {
    // Default value
    myColor = 'blue';

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

class Derived extends Base {
     myColor = 'red'; 
}

// Prints "blue", expected "red"
const x = new Derived();

I was expecting my derived class field initializer to run before the base class constructor. Instead, the derived class doesn't change the myColor property until after the base class constructor runs, so I observe the wrong values in the constructor.

Is this a bug? What's wrong? Why does this happen? What should I do instead?

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
Ryan Cavanaugh
  • 209,514
  • 56
  • 272
  • 235

2 Answers2

41

Not a Bug

First up, this is not a bug in TypeScript, Babel, or your JS runtime.

Why It Has To Be This Way

The first follow-up you might have is "Why not do this correctly!?!?". Let's examine the specific case of TypeScript emit. The actual answer depends on what version of ECMAScript we're emitting class code for.

Downlevel emit: ES3/ES5

Let's examine the code emitted by TypeScript for ES3 or ES5. I've simplified + annotated this a bit for readability:

var Base = (function () {
    function Base() {
        // BASE CLASS PROPERTY INITIALIZERS
        this.myColor = 'blue';
        console.log(this.myColor);
    }
    return Base;
}());

var Derived = (function (_super) {
    __extends(Derived, _super);
    function Derived() {
        // RUN THE BASE CLASS CTOR
        _super();

        // DERIVED CLASS PROPERTY INITIALIZERS
        this.myColor = 'red';

        // Code in the derived class ctor body would appear here
    }
    return Derived;
}(Base));

The base class emit is uncontroversially correct - the fields are initialized, then the constructor body runs. You certainly wouldn't want the opposite - initializing the fields before running the constructor body would mean you couldn't see the field values until after the constructor, which is not what anyone wants.

Is the derived class emit correct?

No, you should swap the order

Many people would argue that the derived class emit should look like this:

    // DERIVED CLASS PROPERTY INITIALIZERS
    this.myColor = 'red';

    // RUN THE BASE CLASS CTOR
    _super();

This is super wrong for any number of reasons:

  • It has no corresponding behavior in ES6 (see next section)
  • The value 'red' for myColor will be immediately overwritten by the base class value 'blue'
  • The derived class field initializer might invoke base class methods which depend on base class initializations.

On that last point, consider this code:

class Base {
    thing = 'ok';
    getThing() { return this.thing; }
}
class Derived extends Base {
    something = this.getThing();
}

If the derived class initializers ran before the base class initializers, Derived#something would always be undefined, when clearly it should be 'ok'.

No, you should use a time machine

Many other people would argue that a nebulous something else should be done so that Base knows that Derived has a field initializer.

You can write example solutions that depend on knowing the entire universe of code to be run. But TypeScript / Babel / etc cannot guarantee that this exists. For example, Base can be in a separate file where we can't see its implementation.

Downlevel emit: ES6

If you didn't already know this, it's time to learn: classes are not a TypeScript feature. They're part of ES6 and have defined semantics. But ES6 classes don't support field initializers, so they get transformed to ES6-compatible code. It looks like this:

class Base {
    constructor() {
        // Default value
        this.myColor = 'blue';
        console.log(this.myColor);
    }
}
class Derived extends Base {
    constructor() {
        super(...arguments);
        this.myColor = 'red';
    }
}

Instead of

    super(...arguments);
    this.myColor = 'red';

Should we have this?

    this.myColor = 'red';
    super(...arguments);

No, because it doesn't work. It's illegal to refer to this before invoking super in a derived class. It simply cannot work this way.

ES7+: Public Fields

The TC39 committee that controls JavaScript is investigating adding field initializers to a future version of the language.

You can read about it on GitHub or read the specific issue about initialization order.

OOP refresher: Virtual Behavior from Constructors

All OOP languages have a general guideline, some enforced explicitly, some implicitly by convention:

Do not call virtual methods from the constructor

Examples:

In JavaScript, we have to expand this rule a little

Do not observe virtual behavior from the constructor

and

Class property initialization counts as virtual

Solutions

The standard solution is to transform the field initialization to a constructor parameter:

class Base {
    myColor: string;
    constructor(color: string = "blue") {
        this.myColor = color;
        console.log(this.myColor);
    }
}

class Derived extends Base {
    constructor() {
        super("red");
     }
}

// Prints "red" as expected
const x = new Derived();

You can also use an init pattern, though you need to be cautious to not observe virtual behavior from it and to not do things in the derived init method that require a complete initialization of the base class:

class Base {
    myColor: string;
    constructor() {
        this.init();
        console.log(this.myColor);
    }
    init() {
        this.myColor = "blue";
    }
}

class Derived extends Base {
    init() {
        super.init();
        this.myColor = "red";
    }
}

// Prints "red" as expected
const x = new Derived();
Ryan Cavanaugh
  • 209,514
  • 56
  • 272
  • 235
  • Instead of using the ES3/ES5 transpiler output for the explanation, it should be enough to desugar the class field initialiser to a proper explicit constructor – Bergi Apr 24 '17 at 19:22
  • 4
    That's quite verbose way to explain such a simple thing, I would say. It's just 'super always goes first'. 'ES7' term is obsolete, it's ES.next now. Considering that this is pro bono self-answered question, the example in the original question isn't very eloquent. The regular question would be likely downvoted because it cannot get a constuctive answer, the snippet lacks the context and it's not clear why the OP does what he/she does. – Estus Flask Apr 24 '17 at 19:29
  • 2
    I wrote this because people are *endlessly* confused about this on the TypeScript GitHub issue tracker https://github.com/Microsoft/TypeScript/issues/1617 and refuse to accept a simple explanation (my "super goes first" comment there currently sits with 7 "thumbs down" reactions) – Ryan Cavanaugh Apr 24 '17 at 19:40
  • One more possible solution depending on the needs of the developer, is to use InversifyJS and IoC to initialize any class they need with properties injected by construction. But again this does not mean that everything should be injected, depends on the use case. – juan garcia Feb 14 '18 at 18:06
  • A number of "OO" languages (Java, C#/VB.NET, C++, etc.) *require* that the base-type ctor (and other initialization) is invoked/done first. In this aspect, TS is "behaving per commonly accepted subtyping patterns" .. granted there are counter-example languages and [ab]use of such different constructs as they allow :} – user2864740 Aug 25 '18 at 02:21
  • 5
    if 'people are super confused' it means the language syntax is super confusing... While this makes sense in regards to backwards compatibility with ES6 classes, it does not make sense from a developer perspective. Being technically correct, and being useful are different things. – Jack Murphy Feb 09 '19 at 16:20
  • Just ran into this issue (in the year of our Lord 2021). While I (like many others) find it weird that Typescript's OOP works this way, thank you @RyanCavanaugh for providing a workable example that the rest of us can generalize from – semore_1267 May 04 '21 at 21:06
6

I would respectfully argue this is, in fact, a bug

By doing an unexpected thing, this is undesired behavior that breaks common class extension use cases. Here is the initialization order that would support your use case and that I would argue is better:

Base property initializers
Derived property initializers
Base constructor
Derived constructor

Problems / Solutions

- The typescript compiler currently emits property initializations in the constructor

The solution here is to separate the property initializations from the calling of the constructor functions. C# does this, although it inits base properties after derived properties, which is also counterintuitive. This could be accomplished by emitting helper classes so that the derived class can initialize the base class in an arbitrary order.

class _Base {
    ctor() {
        console.log('base ctor color: ', this.myColor);
    }

    initProps() {
        this.myColor = 'blue';
    }
}
class _Derived extends _Base {
    constructor() {
        super();
    }

    ctor() {
        super.ctor();
        console.log('derived ctor color: ', this.myColor);
    }

    initProps() {
        super.initProps();
        this.myColor = 'red';
    }
}

class Base {
    constructor() {
        const _class = new _Base();
        _class.initProps();
        _class.ctor();
        return _class;
    }
}
class Derived {
    constructor() {
        const _class = new _Derived();
        _class.initProps();
        _class.ctor();
        return _class;
    }
}

// Prints:
// "base ctor color: red"
// "derived ctor color: red"
const d = new Derived();

- Won't the base constructor break because we're using derived class properties?

Any logic that breaks in the base constructor can be moved to a method that would be overridden in the derived class. Since derived methods are initialized before the base constructor is called, this would work correctly. Example:

class Base {
    protected numThings = 5;

    constructor() {
        console.log('math result: ', this.doMath())
    }

    protected doMath() {
        return 10/this.numThings;
    }
}

class Derived extends Base {
    // Overrides. Would cause divide by 0 in base if we weren't overriding doMath
    protected numThings = 0;

    protected doMath() {
        return 100 + this.numThings;
    }
}

// Should print "math result: 100"
const x = new Derived();
frodo2975
  • 10,340
  • 3
  • 34
  • 41
  • Your proposed emit breaks `instanceof` and also assumes that all base classes would be written in TypeScript, which is not the case. – Ryan Cavanaugh Oct 02 '18 at 16:34
  • Hmm, you're right about instanceof. Would there be any issue with just swapping out the class's name for the helper class name at compile time? Like, the compiler would replace `instanceof Derived` with `instanceof _Derived`. – frodo2975 Oct 02 '18 at 18:34
  • For extending 3rd party libraries, there's no way to control the init order, so it would function as it does today. – frodo2975 Oct 02 '18 at 18:35
  • So now you have one initialization order for TypeScript classes where the class and the base class are in the same compilation, and a different initialization order when they're not. And you have to rewrite the class name in all locations, *and* tell JS consumers of your code to (sometimes!) refer to `_Derived` instead of `Derived`. – Ryan Cavanaugh Oct 02 '18 at 19:00
  • And it's still not compliant with the proposed ECMAScript field initialization order, so when that feature is in your runtime, your class changes behavior depending on whether it's being downlevel-transpiled or not – Ryan Cavanaugh Oct 02 '18 at 19:01
  • I'm not too concerned with js consumers because typescript already has sourcemap support. I wouldn't expect anyone to read the emitted output very often. Typescript already does a bit of complex stuff to implement inheritance. If ECMAScript does go with a different order, then I agree that typescript should go with it for consistency, but it would be great if they would go with this order. – frodo2975 Oct 02 '18 at 19:12