In this video I wanted to share a quick TypeScript tip with you. And that tip is that you can create interfaces based off of classes.
Here in front of me I have some code from a game I’m working on.
Don’t worry about the details just focus on line 410 where we have interface ShapeGenerator extending Generator.
Nothing too exciting there if you’ve already seen my video on interfaces, but if we jump to the definition of Generator… You’ll see that it’s a class and not an interface!
It turns out that TypeScript will let you create interfaces from classes, and it does this in an intuitive way: All the public class fields and methods become part of your interface. And then, of course, you can add more if you need them.
Now, why would you want to create an interface in this way?
Well, for this program in particular, I have a number of classes that are based on Generator, but they are all completely separate in their implementation. However, some of them share these three fields, and I want to be able to do things like create arrays of ShapeGenerators.
Create a new interface based off of the Generator class is a super easy way of making it so that I don’t have to repeat all the fields of Generator, and I don’t have to add any extra levels of inheritance just to be able to treat all my similar classes in similar ways.
You won’t need it all the time, but in cases like this, it’s helpful to know that this feature is there.
That’s it for now.
If you want to dive deeper into 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’ll let you know when the course is released.
Thank you so much for watching! I will see you in the next video.
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.
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.
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.
Hello, everybody! Today I’m going to show you how to use classes in TypeScript. There’s a lot to cover about classes, so for this video we’re going to stick to the basics, and then in future videos we’ll dig into the really advanced stuff.
At their core, classes are a way of describing what data you want certain objects to hold, along with methods that act on that data in various ways. There are a lot of other features built on top of this, but that is the core.
If you come from a language like C# or Java, classes in TypeScript are very similar to what you worked with there. Though there are some important differences in the low-level details, which we’ll be covering in later videos.
Unlike Java or C#, however, there’s nothing in TypeScript that is forcing you to use classes. So if you prefer to write your code in some other way, you are perfectly free to do so. Classes are just another tool you have in your toolbelt as a TypeScript developer.
So let’s start off with a simple example class.
Let’s say we’re making game and we’re planning on having multiple players and a decent amount of logic common to the players to handle things like movement, displaying the players, and so on. This sounds like a good candidate for a class, so let’s create a class called Player.
Creating a class is very simple, we’ll write the class keyword followed by our class name, and then the contents of the class are contained within curly brackets. If you saw the previous video on interfaces, you’ll notice that class definitions and interface definitions share a lot of parallels, which you’ll see more of going forward.
Let’s pause for a moment and note that this is actually a complete class. It’s empty, so it’s not very useful, but before we start putting things in it, I want to show you how to create an instance of this class.
We’ll create a new variable called “p”, and set it equal to “new Player()”.
The “new” keyword is how you indicate that you’d like a new instance of an object that’s defined by a certain class. Player is obviously the name of your class, and your parentheses indicate that this is a function call.
Now, we haven’t actually defined any function here, so what function is it calling? Well, all classes have what are called “constructor” functions, and if you don’t create one explicitly, TypeScript will create one for you. We’ll go into more detail on this in a bit.
Let’s log the variable and see what it looks like.
You’ll see that Node.js is actually very helpful here, showing us the class name, and then the empty curly brackets will contain the data in our object once we actually start adding some fields to our class.
So, how do we add things to our Player class?
The first and easiest way is to simply write the name of the variable, and then assign to it whatever value you want.
Here we’ll say each player needs and x and a y position, so we’ll set both of those equal to zero.
If we run our program again, you’ll see that I have a player with an x and y of 0. Excellent!
These fields are just like the fields of any other object. For example, I can set “p.x” equal to 1, and that will do what you expect.
A note to those from other programming languages: Fields in TypeScript are all publicly accessible by default.
Just to prove to you that we have an isolated instance of our object, let’s create a second variable called p2, and log that one out as well.
And you’ll see it’s only our original object that has changed, just as we would expect.
Going back to our class definition, note that if you wanted to be explicit about the type that you wanted a field to be, you can indicate that in the usual way by simply writing a colon and then the type that you’d like.
What we’ve done so far works well for fields that will always start off at some default value, but what about a case where there isn’t necessarily a correct default value?
For example, let’s say we wanted to give each Player a name. We’ll add the name field and say that it should be a string.
But, what’s a good default for that? Well, an empty string might not make much sense there. Neither would a random name. Plus then we’d have to remember to change it properly after instantiating it.
Probably what makes most sense would be for us to force the name to be specified at the creation of the object. We mentioned previously that there was a constructor function being created for us, let’s define it ourselves now so that we can use it to require a name.
You can do that by going to the class definition, typing “constructor”, parentheses and then curly brackets for the constructor function body.
We’ll add “name” as a parameter to this function, and we’ll assign “this.name” equal to the “name” parameter.
You’ll see that now TypeScript is giving us errors about the places where we’re creating instances of this class because we’re not providing a name.
Let’s name the first one “Tyler” and the second one “Fred”. And now TypeScript is happy again.
A couple points of clarification on constructor functions. First, the special “this” keyword refers to the particular instance of the class that you’re currently working with. Here in the constructor, that’s the newly created object that we’re building up.
Second, the way these default values for x and y are set by TypeScript is by moving the assignment to the very top of your constructor function.
So assigning a value to them on the same line as their definition is directly equivalent to writing “this.x = 0” at the top of the constructor function.
In general, since assigning the default value where the field is declared is more compact than the alternative, I tend to favor doing that in my own code. If you prefer to leave the declaration as only the types, and initialize all the defaults in the constructor that’s a perfectly valid stylistic choice.
Note that if you’re initializing something in the constructor, the field declaration must have a type on it, because otherwise TypeScript will not be able to infer what type it is that you want.
Okay, so we’ve got some data that we’re holding in our objects, let’s create a function to act on that data.
Let’s say we don’t think the output of console.log is quite to our liking, so we’d like some other way of displaying our Players.
Well, we can create a new method by specifying the name, the arguments in parentheses, and then we can specify the return type with a colon after the parentheses.
And we’ll type out a call to console.log() that formats things a little more nicely.
And we’ll run that, and hey! Things are looking pretty good.
One thing you’ll notice down below is that the list of functions that are part of an object aren’t included in the default console.log() output in Node.js. Generally this is fine, since the data will be more important to see, but this is something you should be aware of.
Let’s tie things into the previous video by showing how we can make interfaces and classes work together.
Since we’re working on a game here, I imagine we’ll have a number of different types of objects that we’ll want to treat generically in some kind of way. Perhaps we’ll use some common fields to do collision detection or to draw them or whatever else.
To define what we need, we can create an interface listing what we’ll need from each game object.
Let’s say this is a 3D game, so we’ll need an x y and z position.
In order to indicate that we want our player to be compatible with this interface, we can use the “implements” keyword followed by the name of the interface. Note that if you had multiple interfaces, you can separate them by commas.
Now we’re getting an error about our player not implementing the interface correctly because it doesn’t have the “z” field, so let’s add that.
And now everything is good.
Down lower in the code, we can create an array of GameObjects, and put our two player objects in that array. Since our Player class properly implements the GameObject interface, TypeScript is perfectly happy with this.
Note that the “implements” keyword on our class definition isn’t actually required as you might be used to from other languages. If we remove it, everything is still fine. The difference here is that if we were to remove our “z” field, we now get an error down in our array rather than on the class itself.
So why would you want to use the implements keyword if you can get away without it?
The biggest reason is that it will tell you while you’re focused on just the class definition itself whether or not the class properly implements the interface.
It also serves as great documentation for your future self and other programmers that the specific class is intentionally conforming to some interface.
A third reason, is that some frameworks like Angular will check objects for specific methods. If they exist, Angular will call them, if they don’t, Angular won’t. Including the “implements” keyword and the relevant interface for that specific Angular feature will cause TypeScript to validate that you are actually including that method properly. If not, you’ll get an error, which is helpful.
Okay, we’ll pause here and in future videos we’ll be covering things like inheritance, private, public, and protected fields, static properties, accessors, the nitty gritty details on the implementation details behind classes, and much more.
If you’ve enjoyed this video and want to go deeper, I’m working on a course on TypeScript. You can find it at https://typescriptbyexample.com I’m still working on it but if you scroll down to the bottom of the page leave your email address there I’ll shoot you an email when that course is released.
That’s it for this video! Thank you so much for watching, I will see you in the next one.
Hello, everybody! In today’s video I wanted to show you interfaces, what they are, how to use them, etc. So let’s get started!
What exactly are interfaces? Well, interfaces are a way of ensuring that the objects you’re passing around throughout your program conform to your expectations. You can describe the shape of a paricular object, and the types of all its fields, and TypeScript will enforce that your expectations for that object match reality.
This becomes immensely valuable once your program starts to grow past even a few dozen lines long.
Interfaces are one of the basic building blocks of TypeScript code, and you’ll see them used everywhere. Here I’ve opened a typings file that’s internal to TypeScript itself, and if we search for interfaces we get 86 results just in this file alone! So they’re definitely something you want to be familiar with.
Interfaces pair really well with classes in TypeScript, and we’ll be going into detail on classes in a future video.
Let’s start with some simple examples of an interface. Say we’re writing a web app; we’ll likely have lots of different code that will need to work with user data, so user data seems like a good candidate for an interface.
We’ll create an interface using the interface keyword, followed by the name we want for it. I’ll call this User.
Then we’ll define the fields a user should have. Let’s say we want the name as a string, the time of their first visit as a Date, and their role is going to be either a “admin”, a “manager”, or a “employee”.
That’s a complete interface!
Let’s create a variable that uses this new interface. Like any other variable, we’ll give it name write a colon and then the type, which in this case will be the name of our interface.
We’ll then put in a name, create a new Date object which will hold the current time, and we’ll say the role is “admin”.
So what do interfaces look like when they’re compiled? Well, TypeScript interfaces are “zero cost”, which means when you compile your code they completely go away.
If we open up our compiled code, you’ll see that there’s no trace of our interface to be found.
This means that all the enforcement of the interfaces happens at compile time, and when your program is running, it’s not going to be slowed down by interfaces in any way.
On the other hand, if you’re receiving data from some external source that you’ve defined an interface for, TypeScript won’t be able to tell you if that external source has given you valid data.
So make sure that you write your own validation code in that case to prevent any runtime errors.
If you’ve been following along in the series so far, you’ll have seen me use the “type” keyword to create some new types that were useful for our programs.
The type keyword can describe objects in a similar way that interfaces can, but you generally won’t find it used that way.
Interfaces are the standard way of saying “here’s how I expect this object to look”, whereas the uses of the “type” keyword are more about combining various types in various ways.
You’ll see this more in the video on classes, where we’ll have classes declare that they “implement” certain interfaces, and TypeScript will ensure that they actually do.
In general, if you need to create a type for some specific object, use an interface. Otherwise use the type keyword.
One interesting thing you can do is create interfaces that are based off of other interfaces.
Let’s say we actually want to store some additional data on admin users. What we can do is say “interface AdminUser extends User”.
Currently I haven’t yet listed any fields. But if I were to change my use of type User to type AdminUser, TypeScript will continue to be happy because all the types from User have carried down to AdminUser.
For AdminUsers, let’s add an array of things that they have access to.
You’ll see that I now have this red underline here because the access property is missing. If we give this user access to a few things, TypeScript will be happy again.
An interesting thing interfaces can do is override fields that are defined in interfaces that they’ve extended.
A good example of this would be the “role” field. On AdminUsers, it’s always going to be “admin”, so let’s specify that.
Now if I try to change myself to an employee from an admin, you’ll see that TypeScript is now throwing an error.
Overriding fields like this is pretty useful, and you may see it in TypeScript code you encounter in the wild every now and again.
Another thing interfaces can specify is functions that should be a part of them. Let’s say for our AdminUsers we want some way of logging their name to the console.
You’ll simply write the function name, parentheses, the argument list and then the expected return value of the function.
Since it’s just an interface, we don’t provide any implementation for this function, that’s left up to the actual instances of the object.
We’ll add a function down here to satisfy the interface, and TypeScript is happy with us again.
Note that if we do something like alter the expected return type, TypeScript will helpfully indicate the error so that we know to update the usages of that interface.
Another very helpful thing you can do with interfaces is define optional properties. So let’s define an optional property called “jobTitle”. All that it takes to do that is to add a question mark at the end of the name. And you’ll see that since it’s optional, TypeScript isn’t giving us an error about the jobTitle field being missing from our user object.
Of course, it will give us an error if we define the field with the wrong type.
If you have strictNullChecks turned on, an optional field can only be the type you specify or undefined. It can’t be null. If you need null as a valid value, you’ll need to explicitly spell that out in the type definition.
If you want to prevent people from modifying an object that implements a certain interface, you can describe certain properties as “read only” by prefixing them with the “readonly” keyword.
Let’s create a variant of our User interface that is all readonly called ReadonlyUser. Perhaps we want to make sure that our user interface code is only displaying users, rather than modifying them in any way.
So now if we create a new ReadonlyUser, TypeScript will give us errors if we try to modify any of the fields.
Note that this is a fairly weak guarantee, however. If we create a variable of type User and assign our ReadonlyUser to it, TypeScript will be perfectly happy doing so, because all the necessary fields are present. But now we can modify the ReadonlyUser by way of this plain User variable.
So think of readonly as a light helper or as a bit of documentation to other programmers, rather than a strict guarantee that the field marked as read only will never change.
Moving on: a common mistake that you might make while using an interface is making a typo on one of the field names. TypeScript has a feature called “excess property checks” which aims to help prevent this kind of mistake.
Looking at our AdminUser here, let’s typo the “name” field.
TypeScript is now giving us an error that this unknown field does not exist on type AdminUser, which helpfully let’s us catch our typo.
However, the excess property checks only happen on object literals. So, that’s situations like this where we’re assigning directly to a variable, or if you were to pass an object literal to a function that’s expecting an interface, TypeScript would do these checks as well.
However, if you’re pulling in a variable in some other way, the excess property checks aren’t going to happen.
To show you what I mean, let’s change our variable declaration a little bit, and add a typo on the jobTitle field.
We’ll then create a new variable of type AdminUser and assign our previously created variable to it.
Since excess property checks won’t be used in this situation, TypeScript is only guaranteeing that the minimum required fields are present, which they are. The field with the typo is just ignored as extra data which isn’t important to the interface.
This problem very well illustrates a general principle of how TypeScript interfaces work: TypeScript will guarantee that the object has at least the fields you describe. They might have more fields, they might have extra functions, they might have none of their optional properties, they might have whatever else. But you can be sure that they have at least what you specified.
That’s especially useful to know if you’re going to do something like loop over all the fields in an object. If you do that, TypeScript is only guaranteeing that you’ll see the minimum required fields, but you might see more things depending on where your data came from, so you’ll need to make sure your code will handle that properly.
Generally you won’t run into these kinds of issues on a day-to-day basis, but it’s really helpful to be aware of them for when you eventually do.
Moving on, let’s say you have an object that you’re using to store other objects. For example, we might want to have a mapping between a user’s ID and the user data itself. You can use what are called “index properties” to describe this sort of situation.
Let’s create a MapOfUsers, and we’ll associate a string ID to the user data.
You’ll see here in square brackets we define the type we’ll be accessing with, and then after a colon outside the square brackets, we list the type we’ll return with that access.
Let’s use this new MapOfUsers type. We can put in our existing user, and that’ll work just fine.
We could also define a new user in-line, and TypeScript will enforce that it has the proper values.
TypeScript will also enforce that you use the proper index type. Here we used strings, but we could also switch to numbers instead.
You’ll see that after making that change, TypeScript indicated an error until we switched all of our values from strings to numbers.
The final feature of interfaces I wanted to show you is the ability to modify previously declared interfaces. I don’t see this used very often, but it’s good to know that it’s something that exists.
In order to do this, you simply write out the interface keyword and the name of the interface you’re going to modify. Let’s let’s pretend that this interface user is defined in some file A and this other one is defined in some file B. Perhaps in file B we’re asking the user for their age and we want to indicate that as part of the user document.
You’ll see now down here on the user we’ve created, we’re now getting an error about age not existing. So as simple as that we added a new field to the interface. Perhaps we should mark it as optional.
You’ll want to be really careful when you do this, because adding new required properties could easily break your existing code. Your code will also become harder to follow because now your interface definition is split across multiple places, so it’s hard to get the big picture view of what the whole thing looks like.
Alright! That’s it for interfaces! Stay tuned for the upcoming video on classes, which interact with interfaces quite a bit.
If you’ve enjoyed this video and want to go deeper, I’m working on course about TypeScript. You can find it at https://typescriptbyexample.com Scroll down to the bottom and put in your email address, and I’ll let you know when the course is released.
Thank you so much for watching! I’ll see you in the next video.
Hello, everybody! In our past videos we’ve talked about the most basic types in TypeScript, today I wanted to talk about enumerations (which you might also know as enums).
If you’re familiar with enumerations from other languages like C, C++, or any other language, they’re very similar in TypeScript, though there are a few differences that you’ll see as we go over their various features.
If you’re unfamiliar with enumerations (or if you don’t use them very often) the easiest way to think of them is as a group of related constants that are important to your program.
What this means in practice is that while you’re writing code you might find some mathematical constants that you need to use, or you might find that some piece of your code can be in several distinct states, and you need to represent that in some way.
Say if you’re writing a game, an enemy might be sleeping, or it might be patrolling, or it might be fighting, and you need to represent those different states with some sort of value. The exact value doesn’t necessarily matter, the most important thing is that you can tell each state apart from the other states. This is a great place to use an enum.
Using that example of an enemy in a game, we’re going to create a simple enum called EnemyState.
You can do that by using the enum keyword, then the name of the enum (EnemyState in this case), and then listing the names separated by commas within curly brackets.
A common convention when creating enums is for the members of the enum to be all upper case, with multiple words separated by underscores. However, there’s nothing forcing you to do that. If you perfer some other way of naming them, feel free to do so.
I mentioned moments ago that enums store constants, but I didn’t actually mention any values here, just names. So what value is used for EnemyState.SLEEPING?
Well, let’s create a variable storing that value, and then we’ll console.log() it.
I’ll open a terminal here and run it.
And we get 0! Okay, so we know TypeScript selected 0 for the value of SLEEPING.
Let’s change it to PATROLLING and see what it logs.
And we get 1!
So you can see that if we don’t specify any exact value, TypeScript will automatically use 0 as the starting number, and then work its way up one by one to 1, 2, 3 and so on.
One thing that’s helpful as well is that this EnemyState enum exists as an object at run time.
So, if you’re debugging something or you’re are working with a library that defines an enum and you’re asking yourself “hey what are the valid values of this particular enumeration?” you can simply log the enum object and see the possibilities.
You can see here when we logged it, though, we got more than just a list of values. We can see that the EnemyState enum maps between both the names and the values in both directions.
So if we had some random variable that came from an enum, and we wanted to see a human-readable version of the value, we can simply do EnemyState[x] and get the name of that member of the enum as a string.
If we open up the compiled .js file, you can see how they do this.
This might be a little weird to read at first, because they’re using a trick to save space, but read it from the inside out and it should make a little more sense.
Here they’re mapping between the name of the value and the value itself. And then it turns out that when you set something equal to something else, you can use that as an expression that returns the value that just got assigned. So they’re mapping the value back to the name, just like we saw in our tests previously.
It’s helpful to know at this point that you don’t need to let TypeScript automatically choose the numbers for you. You can add an equals sign to one of the members of the enum, and then all the values that follow will increment from there. You can also specify each value explicitly if that makes more sense for your program.
Now, let’s pause for a moment and compare enums to their alternatives.
The first and most obvious alternative would be to just declare a bunch of constants as normal variables, rather than as an enum. If you’ve only got a couple constants, or if none of the constants are related, that works just fine. But if they’re related, or you’ve got a situation like EnemyState where your main goal is to simply have a few distinct values, enums are the appropriate choice.
The second alternative, which you may have seen in a prior video, is union types.
As a refresher, a union type is a list of different types separated by the pipe character. You can then assign your variable to any of the types that are listed.
This is very much like an enum, and you’ll frequently see them used like enums in TypeScript code.
Enums have two main advantages over union types: First, since it’s all just numbers behind the scenes, they will be more efficient in some cases. Second, enums are available at run time as we’ve shown previously, so if you need a list of all possible values for debugging or display purposes, enums are a great option.
However, union types are quick to write and sometimes easier to think about, so often TypeScript code you see in the wild will prefer them over enums. Which is best to use will depend on your specific use case and your own personal preferences.
There’s a couple less-used features of enums that I wanted to show you before we wrap up.
First, your enums aren’t just restricted to numerical values, you can also put strings in them.
Back in the TypeScript code, you’ll see that if I remove one of the strings TypeScript gives me an error. Unlike numbers where TypeScript can just take whatever you did last and add a plus one to it, TypeScript has no idea what the next string is going to be. So, if you specify string constants you’re going to need to spell them all out.
Enums with strings in them aren’t used very often, but it’s a feature that’s available to you if you’ve got some string constants you need to group together.
The final feature I wanted to talk about are what’s called “const enums”. You can create one by simply putting the “const” keyword at the start of your enum declaration.
This will result in slightly more efficient code, at the cost of being harder to debug should you ever need to. Again, this is something you won’t see used very often, but it’s another tool to add to your toolbelt.
So that’s all there is to enums! I hope you enjoyed this video! If you would like to go deeper into TypeScript I’m producing a course called TypeScript By Example and you can find that at https://typescriptbyexample.com If you scroll down to the bottom you can put in your email address to be notified when the course is released.
Thank you so much for watching! I’ll see you in the next video.