Learn TypeScript #4, Interfaces

March 19, 2019 0 Comments

Learn TypeScript #4, Interfaces

 

 


Transcript:

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.

The main use case for this feature is if you are modifying a library that you’re using, or if you’re monkey patching one of the built in JavaScript objects. Otherwise I reccomend against you using this feature.

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.



Tag cloud