A (somewhat) deep dive into TypeScript constructor intricacies, step-by-step

boscodomingo

Bosco Domingo

Posted on March 5, 2024

A (somewhat) deep dive into TypeScript constructor intricacies, step-by-step

If you've ever wondered how TypeScript's constructor shenanigans work, here's a quick one for you.

You have a TL;DR at the bottom with the key takeaways if you're in a rush!


Unfortunately the TypeScript docs don't really go into any detail at all of how it translates class property initialisers, the constructor shorthand and explicit assignments into JavaScript and how all of them play together, so I took it upon myself to find out.

How does it transpile?

First of all, let's see the result of the simplest classes and constructors:

class InitialiserOnly {
    a: number = 1;
}

class ConstructorShorthand {
    constructor(public a: number = 1) { }
}

class ManualAssignment {
    a: number;

    constructor(a: number = 1) {
        this.a = a;
    }
}

class ManualAssignment2 {
    a: number;

    constructor() {
        this.a = 1;
    }
}
Enter fullscreen mode Exit fullscreen mode

Transpiles to:

"use strict";
class InitialiserOnly {
    constructor() {
        this.a = 1;
    }
}
class ConstructorShorthand {
    constructor(a = 1) {
        this.a = a;
    }
}
class ManualAssignment {
    constructor(a = 1) {
        this.a = a;
    }
}
class ManualAssignment2 {
    constructor() {
        this.a = 1;
    }
}
Enter fullscreen mode Exit fullscreen mode

As you can see, everything pretty much becomes the same thing: assignments inside the constructor, thus at runtime they'll all be equivalent (a === 1). Fairly straightforward.

Then what is the difference? Let's see

What about multiple operations on the same variable?

class ConstructorFun {
    a: number = 2;

    constructor(value: number = 3) {
        this.a = value;
    }
}
Enter fullscreen mode Exit fullscreen mode

Transpiles to:

"use strict";
class ConstructorFun {
    constructor(value = 3) {
        this.a = 2;
        this.a = value;
    }
}
Enter fullscreen mode Exit fullscreen mode

Interesting and unsurprising... a = 3 in this case, as the last operation is the explicit this.a assignment, which results in the value of a being the parameter passed to the constructor.

What if we used the constructor shorthand instead?

class ConstructorFun2 {
    a: number = 2;

    constructor(public a: number = 3) { }
}
Enter fullscreen mode Exit fullscreen mode

Transpiles to:

"use strict";
class ConstructorFun2 {
    constructor(a = 3) {
        this.a = a;
        this.a = 2;
    }
}
Enter fullscreen mode Exit fullscreen mode

So completely the opposite this time around: a = 2. Note that in this case TypeScript complains about a duplicated identifier (rightfully so).

Funnily enough, this also applies to the access modifiers (public/protected/private), meaning this:

class ConstructorFun3 {
    protected a: number = 2;

    constructor(public a: number = 3) { }
}
Enter fullscreen mode Exit fullscreen mode

Results in a being protected, not public. The class initialiser trumps the shorthand again.

How does this matter to me? (TL;DR)

Enough blabbering, here's the takeaways.

Order of execution

  1. The constructor shorthand assignments (constructor(readonly b = 3) {...})
  2. The initialisers (private b: string = "test" //...)
  3. The manual/explicit assignments inside the constructor (this.[prop] = value)

Note that all assignments from the first point happen before the first from the next one.

Order of importance

The opposite way around (explicit assignment > initialiser > shorthand).

Performance

In terms of performance, they all should perform equally well, as the JavaScript they transpile to is the same (operations inside the constructor), the only difference being in the order of operations.

Source code

Go ahead and play yourself! Playground Link


Extra: What about combinations with multiple variables?

Well, let's play around one final bit:

class EverythingAtOnce {
    b: string = "second";
    c: string = "third"

    constructor(public a = "first") {
        this.c = "fourth";
    }
}

console.log(new EverythingAtOnce());
Enter fullscreen mode Exit fullscreen mode

Transpiles to exactly what you would expect:

"use strict";
class EverythingAtOnce {
    constructor(a = "first") {
        this.a = a;
        this.b = "second";
        this.c = "third";
        this.c = "fourth";
    }
}
console.log(new EverythingAtOnce());
Enter fullscreen mode Exit fullscreen mode

There you go, hope this was as useful to you as it was for me!

💖 💪 🙅 🚩
boscodomingo
Bosco Domingo

Posted on March 5, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related