A (somewhat) deep dive into TypeScript constructor intricacies, step-by-step
Bosco Domingo
Posted on March 5, 2024
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;
}
}
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;
}
}
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;
}
}
Transpiles to:
"use strict";
class ConstructorFun {
constructor(value = 3) {
this.a = 2;
this.a = value;
}
}
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) { }
}
Transpiles to:
"use strict";
class ConstructorFun2 {
constructor(a = 3) {
this.a = a;
this.a = 2;
}
}
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) { }
}
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
- The constructor shorthand assignments (
constructor(readonly b = 3) {...}
) - The initialisers (
private b: string = "test" //...
) - 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());
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());
There you go, hope this was as useful to you as it was for me!
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
March 5, 2024