Transcript:
Hello, everybody! In the previous video we talked about the basics of classes, today we’re going to go a bit more in-depth into the more advanced features of classes. So, let’s get started!
Starting off simple, if you saw the video on interfaces, you’ll remember the “readonly” property. Just like in interfaces, you can mark class fields as “readonly” if you want them to stay constant.
While it may be “read only”, it needs to get an initial value from somewhere. And that somewhere is either in-line here when you declare the field or here in the constructor.
If we try to create a function that changes the name,
You’ll see we get an error “cannot assign to name because it is a read-only property”, which is correct.
So, if there’s a value you want to keep constant throughout an object’s lifetime, readonly is your way of doing that. However, as I mentioned in the previous video on interfaces, read-only is only enforced at compilation time by TypeScript.
Nothing in JavaScript is going to prevent a readonly field from being modified. So if you’re interacting with non-TypeScript code, or with TypeScript code that uses the “any” type a lot, there is a chance that your read only fields could get changed.
Moving on from “readonly” properties, let’s talk about static properties.
If you’re familiar with the term “static” from physics, this is nothing like that at all. “static” is a fancy keyword that lets us define properties on the class itself, rather than on the instances of the class.
For example, we might want to keep track of the number of Player instances that we have created. We could do this by creating a static playerCount field, and then incrementing that by one in the Player constructor.
If we scroll down in the code a little and try to access “p.playerCount”, you’ll see that is an error.
To actually access the the player count, we’ll need to do capital P Player.playerCount.
Static properties are helpful for anything that you want to share across all instances of a class. Frequently you’ll see them used to contain constants that are the same across all instances.
For example, players might have a maximum speed that they can move at, so we could create a static “maxSpeed” field to hold that value.
Similarly to normal fields, static fields can be read only so if maxSpeed is never going to change throughout the course of the game, then it would make sense to mark it as read only to ensure that we don’t accidentally change it in some other part of our program.
Now, if you’ve used any other object-oriented programming language such as Java or C#, you’re probably familiar with public, private, and protected members of a class. TypeScript has the same concepts.
In the previous video, I mentioned that all fields and methods are publicly accessible by default, and that means that the “public” keyword is implied on every field and method.
So while we could put the “public” keyword in front of all of these definitions, there’s no real need to do, since it’s equivalent to putting nothing there at all.
Now, one of the principles of good object-oriented design is that you should provide a simple, clean public interface to your object, and hide all the gory implementation details that aren’t relevant to other parts of your program.
This makes it easier to change the internal workings of a class without worrying about breaking any other part of your program, and it prevents internal methods from being misused.
To illustrate this, let’s imagine that for every frame in our game, we want objects to be able to run any logic they need to run to keep the game moving forward. To facilitate this, we’ll change our GameObject interface to include an “update” method that will contain this logic.
So, our Player class is going to need to implement this update method.
Now in order to properly move the player, there might be some complicated physics calculations that we need to run. Let’s create a “calculateMotion” method to represent that.
The logic in “calculateMotion” won’t be relevant to any other object in the game, and we don’t want anybody accidentally calling it and doing something like moving our player twice, or something else equally as weird. So it makes sense to mark it as “private”.
The “private” keyword makes it so that only the class itself can use the method or field. If we scroll down and try to call “calculateMotion” on the instance of our Player class, TypeScript gives us an error saying it’s only accessible within the class itself.
It’s important to understand that private fields and methods don’t count towards whether or not something implements an interface. Say if we set the “z” variable to private, we’ll get an error that Player incorrectly implements the GameObject interface.
Even though technically the Player has that field, nobody can use it and so TypeScript correctly calls that an error.
One other thing to note is that similar to readonly, the public-private distinction is only handled by TypeScript at compile time.
If we open up our compiled JavaScript file, you can see we’ve got our “calculateMotion” method and we’ve got our “z” variable and there’s nothing different about these compared to anything else in this file.
Nothing in the resulting JavaScript code is actually enforcing that these properties are private. TypeScript will prevent you from accessing them improperly in your TypeScript code, but when we get to the JavaScript runtime, everything is public and accessible.
This is helpful for debugging, but if you’ve got a situation where one TypeScript team of developers is working with another plain JavaScript team of developers, you’ll to make sure to communicate what’s private and what’s public.
Alright, I mentioned earlier that there was one other level between public and private, and that’s “protected”.
A “protected” field or a “protected” method is similar to private in that only the class itself can access the property, but with the added distinction that if you have another class which inherits from your class, that child class is able to access the property without restriction.
We haven’t talked about inheritance yet, so I’ll show you a more concrete example of this in a few minutes.
One other thing that I wanted to mention before we move on, is an alternate way of writing what we’re doing with the name field.
Currently we’re declaring it as a public field, and we’re setting its value directly from the constructor argument.
This is actually a really common pattern, and so TypeScript has a way of doing it in 1 line rather than 3.
Let’s get rid of our declaration line, and our “this.name = name” line, and instead we’ll simply put the “public” keyword in front of the name argument.
This has the exact same effect as the 3 lines we had earlier: “name” is declared as a public property, and when it’s passed into the constructor as an argument, it gets directly assigned from that argument value.
This is exactly equivalent to what we had earlier.
Just to be clear, this works for any of the accessibility modifiers except for “static”, so if we wanted this to be “protected readonly” instead of “public”, that is a perfectly valid thing to do.
If we run our program, you’ll see everything is doing exactly what we’d expect.
Okay, on to inheritance!
At a high level, inheritance is a feature that let’s you create new classes that are based on the implementation of existing (parent) classes.
For an example of how to use inheritance, let’s say that for the game we’re making, we’ll have different types of players that share some common functionality.
So our existing Player class is where all the common functionality will be stored, and then we’ll create more specific classes for each type of player that will include whatever differentiates that specific type.
Let’s say that in our game, one type of Player will be able to shoot at enemies with guns. To support this, we’ll create a Gunner class.
We’ll write class Gunner extends Player.
And what this will do is cause Gunner to “inherit” all the fields and methods that the Player has.
Gunner will have an x, y, and a z position. An update method. And so on. Anything new we add to Player will also end up as a part of Gunner.
Even the constructor is carried over, so we could swap out our usage of new Player to new Gunner. And if we go to our Terminal and run that.
You can see Node.js telling us this is now an instance of the Gunner class, but otherwise everything else is working exactly the same as it did thanks to inheritance. Since Gunner has everything Player does, we can treat our Gunner exactly like a Player.
Going back to Gunner’s implementation, we can add things to this in exactly the same way we can add fields or methods to any other class.
For example, we could add a field that keeps track of the number of bullets.
Let’s say that in addition to the name of the Gunner, we want the type of the gun to be specified when a new Gunner is created. We’ll start by creating a new constructor that takes both a name and a gunType.
You’ll see that TypeScript is giving me an error “Constructors for derived classes must contain a super call”. What does that mean?
It means that in order for our object to be properly constructed, we must call the constructor of our parent Player class. You can do that by using the super keyword, and then we’ll give it whatever arguments that the Player constructor needs. Here we’ll just pass in our name, and everything will be happy.
The super keyword is also used to refer in general to any other method you may want to call on the parent class.
Say, for example, we wanted to have our own custom update function for our Gunner. Perhaps every frame they should gain an additional bullet that they can shoot.
But, we still want to use the same movement logic that is defined in our parent class. So we’ll need to call Player.update(). We can do this by adding a call to super.update() as part of our update method, and then that original method will be called.
It’s important to note that that without the super.update() call, the Gunner class would have it’s own completely isolated implementation of update(), the original update code defined in the Player class would go unused in Gunner.
This is often a valid option, as some classes may want to completely override whatever functionality their parent class had implemented.
Previously we mentioned protected fields as an alternative to private fields. Let’s show an example of that here. We’ll change our calculateMotion() method to be protected.
And you’ll see if we try to call calculateMotion() within our Gunner’s update method, that’s perfectly acceptable.
However, if we change calculateMotion() to be private instead of protected, Gunner’s use of that method becomes an error.
So, the protected modifier lets subclasses access the field or method, private keeps it so that only the things within the bounds of that original class definition can access it. Both private and protected prevent access from any other part of the program.
Another inheritance feature that is frequently used is something called an “abstract class”.
An abstract class is a class that’s not meant to be used on its own.
As we said earlier, this Player class is meant to contain functionality common to all player types, but we’ll probably never want to create just a generic player.
This means it’s a good candidate to become an abstract class.
If we add the abstract keyword, you’ll see down below in our code we get an error where we’re directly creating an instance of Player. You cannot create an instance of an abstract thing. We’ll have to switch that to Gunner.
The abstract keyword also comes into play in one other place: method declarations.
We can put the abstract keyword in front of our update method, and this will indicate that this is something we want all child classes to implement, but the parent class won’t be providing any sort of default functionality itself.
If we go this route for our game, this is basically equivalent to saying that we know every different type of Player will need to update itself in some way. But the exact details of how they update will be so varied, that there’s no point in providing some sort of default implementation.
Just to show you that this is being enforced, if I remove the update method on Gunner, you’ll see that we are now getting a complaint about “Non-abstract class Gunner does not implement inherited abstract member update from class Player”.
And that wraps it up for this video!
If you’re interested in going deeper with TypeScript, I’m working on a course called Learn TypeScript by Example.
You can find it at https://typescriptbyexample.com If you scroll down to the bottom, you can put in your email address and I will let you know when the course is released!
Thank you so much for watching! I will see you in the next video.