Latest news about Bitcoin and all cryptocurrencies. Your daily crypto news habit.
Typescript and JavaScript are both awesome languages. To a lot of developers Typescript feels more familiar because it offers concepts we normally encounter in traditional programming languages. Inheritance is one of them.
Due to backwards compatibility we generally transpile our code to Javascript. ES5 itself doesn’t contain classes nor an extend keyword.
So how does this work then? Typescript uses syntactic sugar to “mimic” the class and inheritance behaviour. It creates kind of an illusion of those concepts. Let’s see what I mean by that.
In this blogpost we will dive deep. On our way we will encounter a lot of concepts. It is important to wrap your head around those concepts. Take your time and make some breaks if necessary. You don’t have to understand everything in one read.
Let’s setup a new TypeScript project to illustrate what actually happens behind the curtain. To create a new empty Typescript project let’s run the following command:
tsc --init
This creates us a tsconfig.json which the TypeScript compiler will use to compile our code. Currently it won’t do anything because we haven’t written any code yet. Let’s go on and do so by adding an index.ts:
The Foo class acts as a base class which Bar derives of. Bar accepts a name and a favourite food as a constructor parameter. It passes the name via super call to Foo and assigns the favouriteFood parameter to a private property.
We then intstanciate bar1 and bar2 and call greet and talk. Bar is only able to greet because it extends Foo. Nothing fancy here.
The code above shows the syntactic sugar I was talking about in the beginning. It allows us to write inheritance in a classical way that we are used from other languages like Java or C#.
But how does this work then? Let’s run the „tsc” command to have a look at the „unsugared“ version.
Don’t spend too much time to investigate this code — we will cover that later.
Wow! That’s a lot of extra code. On a first glance we can see that all the sugar disappeared — no classes anymore. But at least we recognize some of the things we wrote before.
We can still see Foo and Bar which now turned into “iffe’s”. IFFE stands for “immediately invoked function expression”. As the name indicates it represents a function that gets immediately executed 😉
But there’s a lot of extra stuff added. What’s the purpose of this __extends method?
To really understand this code let’s first take a step back and write the same functionality in plain old JavaScript. Back to the roots! ❤️
Inheritance in vanilla JavaScript
This code does the exact same as the Typescript version. Even though the code seems to be simple there are a lot of things to understand. It is important that we get the mental concept behind it.
To visualize those concepts we will use the same graphical representation Kyle Simpson (Kyle) used on his „You don’t know JS“ series. By the way. If you haven’t read them yet, I highly recommend you to do so. They are great!
In his book about “this and object prototypes” Kyle represents functions with circles and object with squares. So let’s see how the JS code from above would be represented in such a graphical representation.
👀 Follow me on Twitter or medium to get notified about the newest blog posts and interesting frontend stuff!
- When line one executes it’s going to create a function Foo. Notice that it also creates an object which is the prototype of the Foo function.
function Foo(name) {this.name = name;}
The function Foo contains a .prototype linkage to an object. As the linkage indicates, the object acts as Foo’s prototype. But what about the .constructor property?
Don’t get confused by it’s name. It has nothing to do with construction. It’s just an internal linkage from the prototype back to the function.
2. Let’s go to line 5. In this line we add the greet function to the prototype.
Foo.prototype.greet = function () { console.log(`Hi I am ${this.name}`);};
3. Line 9 then creates the Bar function:
function Bar(name, favouriteFood) {this.favouriteFood = favouriteFood; Foo.call(this, name);}
4. Line 14 is where things become interesting.
Bar.prototype = Object.create(Foo.prototype);
Object.create is a method that’s around since ES5. It basically creates a brand new object and links it to the prototype we pass in as an argument. In other words we create a brand new object that is linked to Foo’s prototype.
We then take this Object and assign it to Bar’s prototype. This results in the following picture.
Notice that the linkage between Bar.prototype and Foo.prototype is represented by [[ Prototype ]]. The [[ Prototype ]] is just an internal linkage from one object to another object.
Probably you already heard about “Protoype Chain”? The prototype chain is the reason why instances of bar can call the greet function. It allows objects to delegate a method call to a different object. The [[ Prototype ]] is part of this chain.
So basically if somebody calls greet on bar it will be checked if greet exists on Bar.prototype. If not — it follows the internal [[ Prototype ]] linkage until it finds a greet method or until the chain ends. If the chain ends it returns undefined.
While [[ Prototype ]] is an internal linkage there is also a public linkage called __proto__. This linkage was invented by Mozilla but has never been standardized. Nevertheless everybody adopted it except IE.
5. Currently bar is quite friendly, it is linked to Foo and therefore able to greet. But it is still very shy. Line 16 to 18 changes that. Bar will now tell you what his favourite food is, which indeed is very useful ;)
Bar.prototype.talk = function () { console.log(`${this.name}: I love ${this.favouriteFood}!`);};
6. Ok. Cool. We created this whole concept but the current code does nothing. So let’s create objects and call some methods on it.
var bar1 = new Bar('bar one');var bar2 = new Bar('bar two');
The picture is now complete 👨🎨 (actually there is still more stuff going on but that’s beyond the scope of this post)
Click here to tweet about this article 🐥
bar1 and bar2 also get linked to talk. Thanks to the linkages we now have the correct prototype chain and can call greet and talk on bar1 and bar2.
We now clearly understand what the vanilla JS code does and what the mental concept behind the code is.
Cool, but what about the Typescript code?
Yes, the goal of this blogpost is to take a deep dive to Typescript inheritance. We will do so in a second. The things we have done so far were just necessary steps to better understand the next section. So now that we are ready, let’s dive! 🐠
Let’s start by looking closer at the lines from 15 to 36. Our Typescript classes disappeared and almost turned into our ES5 code we wrote before.
Foo and Bar are now wrapped inside an IIFE (Immediately-invoked function expression). The IIFE allows us to capture the base object as a variable.
If we compare it with the plain JS code we wrote, we can see that our Object.create method is gone. Instead we now see a __extends method which is called inside Bar. This method is important to understand as it is responsible for the “inheritance” magic. So let’s have a closer look.
__extends
We will split __extends method into two parts. First we have a look at the extendStatics method and then we move on to the function that get’s returned.
extendStatics is a function that gets called with two parameters — d and b. d represents the derived class (Bar) and b the base class (Foo).
First it is checked if Object.setPrototypeof exists. If not then
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; })
will be applied. If both checks return false we will apply the third function which is
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
All of those function basically achieve the same. The checks are necessary due to backwards compatibility.
The last method is the one that will be used if the first two are not supported. It is also the most readable and explains best what extendStatic does.
It checks if a property exists on the base class. If so, this property will be copied to the derived class. Generally spoken it copies the static members of the base class to the child class.
Ok. This was the easy part. Let’s have a look at the harder part. We can do this! 💪
Rebuild prototypical inheritance
This part is quite tricky and may require some reads to understand it.
We skip the first line as it is clear. We just call the extendStatics method described above.
On line 3 we create a named function called __. We will discuss the meaning of it soon. I think the best way to understand this code is by looking at the last line first.
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
Read this method carefully multiple times. Don’t worry if you don’t know what this method does. This line of code contains a lot of logic.
To explain this method better I prefer to refactor this line to a more readable version. Let’s refactor!
if (b === null) { d.prototype = Object.create(b);} else { __.prototype = b.prototype; d.prototype = new __();}
Ok. As we already know — b equals Foo and d equals Bar. By the time we enter this function b is Foo and therefore defined, which results in the execution of the else part.
To visualize things I am again going to use the notation of functions as circles and objects as squares. At the time we enter the function, b and d are already defined.
Next we the following line gets executed:
function __() { this.constructor = d }
This results in another function called __ and its prototype. We also get the already covered .constructor and .prototype linkages.
Then the first line inside the else statement of our refactored code gets called.
__.prototype = b.prototype;
We change the linkage of __.prototype to b.prototype.
Great! Almost there. Only one line left. To understand the last line one more prerequisite is required. We need to understand the effects of the new keyword on a function call.
The new keyword in JavaScript differs from the new keyword in traditional languages. In JavaScript a constructor is just a function that gets called with a new Keyword.
Each regular function can be called with a new Keyword. There are 4 things happening when you call a function with the new Keyword.
- A brand new object get’s created.
- This object get’s [[Prototype]] linked.
- The object get’s set as the this binding.
- The object get’s returned.
In the last line we call __ with new
d.prototype = new __();
By calling new on __ we create a brand new object out of thin air. This object then gets linked to the prototype of the __ function which is Foo.prototype. We then take this object and assign it to Bar.prototype.
Wow, looks quite familiar to the pure ES5 diagram, doesn’t it? But what happened to the talk method? Shouldn’t it be on Bar’s prototype?
The reason it is not on Bar is simple. It hasn’t yet been added.
We can see that Bar.prototype get’s added after we executed the __extends method. At the end we then create bar1 and bar2. The complete diagram then looks like the following:
As you may have noticed it is the exact same as the plain ES5 diagram.
Conclusion
Typescript is an awesome language and offers us a lot of benefits over Javascript. Nevertheless I have seen way to many frontend developers that use TypeScript but have no clue about JavaScript.
Always remember that at runtime it is JavaScript that get’s executed. Therefore in my opinion it is really important to have a solid JavaScript understanding.
🧞 🙏 By the way, click (up to 50x) on the 👏🏻 clap 👏🏻button on the left side if you enjoyed this post.
Claps help other people finding it and encourage me to write more posts 😜
Feel free to checkout some of my other blogs:
- Typescript method overloading
- Demystifying Observables
- Findings about RxJS marble testing and the TestScheduler
Typescript inheritance deep dive 🐋 was originally published in Hacker Noon on Medium, where people are continuing the conversation by highlighting and responding to this story.
Disclaimer
The views and opinions expressed in this article are solely those of the authors and do not reflect the views of Bitcoin Insider. Every investment and trading move involves risk - this is especially true for cryptocurrencies given their volatility. We strongly advise our readers to conduct their own research when making a decision.