What do you want to learn?
Skip to main content
Getting the Most from the TypeScript Compiler
by Thiago Temple
This course, Getting the Most from the TypeScript Compiler, will teach you advanced techniques of TypeScript, how to rely on the compiler to avoid errors, how to have rapid feedback, and how to improve the code maintainability.
Start CourseBookmarkAdd to Channel
Table of contents
Activating Additional Compiler Checks for Your Project
Introduction and Overview
Before we really begin changing the code, I'd like to go over the demo. As I said, this is a web application built using Angular, but I like to reiterate that you don't need to know any Angular for this course. I'm assuming you have Node and NPM on your computer. If you don't, those are easy to install. Just go to nodsejs.org, and follow the instructions there. I'm running on Node 9, but the minimum version required is Node 6.9. I also want to make it clear that I'll be using TypeScript 2.6 for this course. The TypeScript team has been updating the language constantly, and they have improved lots of type checks, compiler messages, and adding new features, so using the latest version is a good idea. I understand that if you have an existing code base and try to upgrade the TypeScript version, you may encounter new errors and you'll need to fix those. If fixing those errors is something you can do, that's great because those will probably be to the benefit of your own code. If not, TypeScript 2.6 added a way to ignore compiler errors on a line-by-line basis if that is something that interests you. Most of what you'll be seeing during this course is present in version 2.1, but I'll be using newer features released after that. Just keep that in mind. With Node installed and the service code downloaded for the demo, just open the console on the folder and run npm install. After a few seconds, you have all the dependencies needed to run the application. Now you just have to type npm start. This will compile the application and start a web server so that we can see the application running. Just open a browser and navigate to local host on part 4200. This task will also be watching for file changes, so whenever you modify a file, it will get compiled automatically. Finally, this console will be a good place for us to see if our application contains any errors. Right now, as you can see, the application's compiling successfully.
To start, I have the tsconfig.json file open on my editor. As you probably know by now, this is the file that sets the options for the TypeScript compiler, and I have here a list of options that are deactivated by default on the compiler, and we'll be activating one by one during the course of this module. First, we'll activate the noUnusedLocals option. Whenever we change the tsconfig file, we need to restart the task in the console. You can do this by pressing Ctrl+C, and then we just need to start it again. As soon as we do that, we'll see some errors in the console. Those are the consequences of activating the noUnusedLocals option. Let's take a look at what this really means, and I will start by looking at the first file in the list of errors: create-report.component.ts. What we can see here is that we have a bunch of imports at the top of the file that are not being used. This happened because of the refactoring I did previously, where I moved some code from this file to another one, and I forgot to remove the imports. This is a very straightforward and simple to fix issue. All we have to do is remove the imports. There are no bigger consequences, so let's do it. The next file with errors is app.module, and I know that because the console is showing the list of files with errors, and the list got updated when I saved the previous file. From now on, I won't show you this list, but keep in mind that you can check the console to see if there are any errors. Let me open this one. Again, TypeScript is showing the same error, we just have a few unused parts, and in this case, I'm importing from the Angular Material Library, which is a library that has a lot of different components. The problem with this is that I'm importing components from this library and not using them, but just because I'm importing the components, they are not part of the code a user of this application has to download. So removing imports will make the final code downloaded by the user smaller. The next file check is create-report-item.component, and in this file, we can see a different case, we can see a private method of this class, a component class that is not being used in the class. So I can safely remove it. Also, the constructor has an argument called data that is not being used anywhere in the class. A quick note about component constructors in Angular. In Angular, when we have a component much like this one, and we can see that it's a component because it has the component decorator on both the class declaration, the constructor of the component is called by Angular itself, and Angular has a dependency injection mechanism that will provide all the required arguments of a given constructor. In some cases, we need to help Angular's dependency injection, and that's why there is this inject decorator. In any case, this argument is not being used, so I can remove it as well. In the file reports component, we can see another import not being used. A private field called isLoading also not being used. Another constructor argument that we can remove. One thing to note that as soon as I removed this code, other dependencies became unused and now I have also to remove them. The final file that I need to fix is app-routing.module, and it only has an import not being used; and that's it for our first compiler option. As you can see, activating it and fixing the errors that come with it is pretty straightforward. The main benefit you get from this is a code with less noise. Code that is not being used is removed, and as a result, the files get cleaner. The next option will be
The next one is noFallthroughCasesInSwitch, and it's also a pretty simple one to fix. I only have one error on the reportitem.service file. Let's take a look. We can see that the case keyword is underlined here, and that's because inside of this case branch, there is no return nor a break keyword. So it means that when the code gets to this portion and because it doesn't return nor break it, we'll continue executing until some case branch does that, or the switch statement is finished. This is generally a bug, and in this code, it really is a bug because I forgot to return the value from the function, validateTraining, but if you had the switch statement and that was really your intention, I have two suggestions for you. First, refactor the code. There are better and less confusing ways to share reused code, and if you can't do that for some reason and want to keep the code as is, you have a second option, and this is new on TypeScript 2.6. You can ignore errors on a line per line basis. The way you do that is by adding a comment above the line with an error with the text @ts-ignore, and that's it. No more errors for you. This comment works for any TypeScript errors, not just this one, but I would advise caution with this approach. The reader of this code may not know why this comment is there and might think that it's okay. Personally, I don't think it is. I think we should fix as much errors as we can, that's the whole goal of using TypeScript. But if for any reason you need to do this, I'd strongly suggest that at least you leave some comment on why you did it. In my case, it's really an error, so I'll fix it by returning the result of the function. Now notice that fall throughs can still exist. The next case, transport, is an empty case that falls through so it will be handled in the same way as travel. That's okay because it's clear that they are both the same, but as soon as I add some code to the transport case branch, it becomes an error if I don't explicitly break the branch execution.
We only have two more options to activate, and the last two are in my opinion, the two where we get the most value, but they are also the two that will take more work if you are activating them on existing code base. In a nutshell, noImplicitAny will yield an error whenever a type is not defined explicitly, and the TypeScript compiler cannot infer a type itself. If you are using TypeScript, chances are that one of the reasons for that is because you want the type safety that the language provide, and having values of type any, especially when it's not intentional, will get in the way of type safety. As soon as I activate the option and restart the task, we can see again a list of files with errors. I'll begin by taking a look at app.module, initApp is a small function that will get called when the app initializes, and its job is to start the messengerWatcher service by calling the watch method in an instance of the messengerWatcher class. In this case, I forgot to define the type of the parameter for the function. In a similar way, initMessengerWatcher is a function responsible for creating the service messengerWatcher. Because messengerWatcher has a dependency on another service: the reportDataService, it will receive it and pass it to the new messengerWatcher service. Again, the issue here is pretty simple to fix. Just add the type to the parameter. On the reportItem.service file, there is a similar issue with another parameter missing a type. In this case, the type missing is a ReportItem. On the base-report.component file, it's a different issue. There's an array, and the array is being initialized with an empty array value. But because it's empty, TypeScript cannot know which types of values will be added to the array. TypeScript considered this an array of any values. The simplest way to fix this array is to explicitly type it. In this case, we are talking about an array of ReportItem. So I could just add this type to the variable and all will be good. But if a closer look at the code, we can see that what it's doing is creating a copy of an array and adding a new ReportItem to the end of the array, and then calling next to signalize a data change. So instead of just adding a type to the variable, I'm going to do a quick refactoring. I'm copying all the elements from the original array and adding the new received report item to the end of the array. If you don't know how the spread operator works, I'll once again recommend you to take a look at Brice Wilson's course: Advanced TypeScript. These are all the errors that I have in this demo, and chances are that you are going to find most of the time similar issues. Arrays initialize without types and parameters with all types. Most of the times it's a matter of identifying the correct type. Some other times, you're going to have to create a new type for what we're doing, and sometimes you should remember that you don't need to use a previously defined name type. You can define an anonymous type for what you are doing. Just as an example, let me create a quick function, and this function will check if some item is approved. Again, this is a small codebase, but in a bigger one it might be hard to find the type for this parameter. I could go spend some time looking for it, but within this function, I know that we are talking about an object that should have a property called Approved of type Boolean. So I can define an inline anonymous type for this function, and in this case, it doesn't matter whether dysfunctions called, as long as the object being passed respects this contract regardless of its type, the application will still compile.
In this module, we saw how to activate and what are the consequences of activating a few of the TypeScript's compiler options. We saw how we can have the compiler check if the code has UnusedLocals and UnusedParameters. We saw how the compiler can detect when we forgot to leave a case from inside of the switch statement. Having types is one of the main benefits of using TypeScript, and it's good to know that we can use the compiler to avoid the any type when we don't want it. Also, we know now that we can rest assured that functions are always returning the types they say they are, and finally, who likes to check for nulls? I certainly don't, so let's have the compiler do it for us, right? In the next module, we'll see how we can use some of the more advanced features of the language to type less, to avoid duplication, and to have safer types.
Using Advanced Features
Hello. My name is Thiago Temple, and welcome back to my course: Getting the Most from the TypeScript Compiler; and in this module: Using Advanced Features, we will see some of the more advanced features of the language, and while you're using these features, the goal remains the same. We still want to have the compiler to work for us. It's not about using a hidden corner of the language. It's about keeping the code strongly typed, even when we try to make it flexible. We are going to see how to achieve the type safety, while dynamically accessing properties of an object using the keyof keyword. It's also about writing less code when possible because the compiler can generate new types for us based on other types, and we'll see how to do that using mapped types. Next, we are going to see what are the benefits of using union types. Syntactically, union types are very simple, but they are also very powerful and can help us ensure a more type safe code, avoid optional arguments, and have a stronger designed application. Finally, we'll see how to use the compiler so we can be sure we are covering all possible logical cases in a given business scenario with the use of discriminated unions.
Let's begin with the keyof operator, and to understand that, we'll change some of the code we have in the ReportItem service file. This is the file I'm using to enforce the business rules when creating a new item for a report, and one of the things I want to validate is that the required fields are filled by the user. As a note, I understand that there are other ways to make a field required, but stay with me in this example, because this is an exercise to understand how the keyof operator can be used. So let me begin by writing a small function that would check for the required fields, and to keep this example simple, I'll just return a Boolean with true when all required fields are filled, and false when at least one of them is not. In this function, I could check for each property manually, and I could have something like a return statement that would check if each property of the item object has a true value; meaning they are not enabled string, have the false value, or 0 in the case of amount. And we don't need to worry about nulls and undefined anymore since they are not part of the accepted values unless we made the properties optional. I could do that, but that's a tedious and a repetitive code and we want to avoid that. Also, if at some point a new property is added to the ReportItem type, I'll have to remember to add it to this check if I wanted to make it required. A second option would be to iterate over each member of your ReportItem object and check if it has a value. One way to do this is by using the Object.keys function. Object.keys returns all keys of a given object, and the object we want to iterate over is the item. So the result of calling Object.keys passing the item is an array of strings with the names of all members of the item object. With that, we want to return true if all required members have a value, and false if any of the values is not filled. Whenever we have an array of values and want to turn it to a single value, we use the reduce function. If you've never use the reduce function of an array, I'm going to walk you through it. If you know how to use a reduced function, this is just going to take a moment, so stick with me. The reduce function takes a first argument, which is a function that will iterate over each member of the object, and a second argument that is initial value. So let me just create a placeholder for this function, and I'm going to pass true as the initial value for the reducer. So what I'm seeing is that the function is valid by default. Now, the callback function that will check for each argument also expects two arguments. The first one that I'm calling: isValid, is the result of the iterations of calling reduce. So the first time this function gets called, the value of isValid is whatever value was passed as an initial value for the reduce function, in this case, true. For us, if this value is false, that means that at least some property is not filled and I don't need to check other properties. So I can just return isValid, which will carry the false value during the auto iterations and will be the result of calling reduce. If this is true, I'll use the second argument of this function, which contains a key for the item object, and with that I'll check if the member of the item object with that key contains any value and return true if it does, and false if it doesn't. Now the TypeScript compiler is complaining with an error that says: Element implicitly has an any type because type ReportItem has no index signature, and what does that mean? Well, the index signature issue is because the memberKey variable is of type string, and when we try to access a property of the item object using a string, we're trying to access one of its members, but the compiler cannot know which one it is because it's just a string. So it automatically says that whatever value it finds with the index or key, it will be of type any because there's no way to know which type it is. Now here's a trick. Because we are calling Object.keys on the item object, we know that it's not returning a simple array of strings, it's returning an array of strings that are indexes or keys of the item object. So if I explicitly type the memberKey with the type of keyof ReportItem, the error goes away, and why is that the case? If I mouse over the memberKey argument now, I can see what are the possible values of this argument. TypeScript knows now that the only possible values of memberKey are the keys of the type ReportItem, and when accessing the item object with one of its own memberKeys, it can infer the types. Now, this is better, and if someone adds a new member to the ReportItem type, it will automatically get validated by default. But this code has a bug. One of ReportItem's members has received is of type boolean, meaning that it can either have a false or true value. So it will always have a value, but if that value is false, this expression will evaluate false as well, and our validation code will return false as if the object is invalid, which is not the case, so I need to ignore that property. A simple way of doing this is to exclude all members that I don't want to validate. I'm not going to check for this one specifically. Let's say that in the future I may decide to add other properties that can become optional. I'll create a variable called exclude, and I'll assign an array with the members I want to ignore. Right now, all I want to ignore is hasReceipt. Next, I'm going to check if the member is on this list, and I will return true if it is, and no check for its value. If it's not, the value will be checked. That works, but if I had made an error when typing the member here, I'd still have a bug. So to prevent that, I'm going to explicitly type the exclude constant with the keyof ReportItem. Actually, it's an array of keys of the ReportItem. Now, if I have made a mistake and misspelled hasReceipt, then this code won't compile because TypeScript is assuring me that all values from this array are actually keys of the type ReportItem. Now TypeScript is even smarter. Let's say I have a special check that I want to do for the date. I can check in an if statement if the member key is equal to date, and note that I have IntelliSense here. TypeScript already knows the possible values that I can compare member key to. Now, inside this if block, TypeScript also knows that the value of memberKey is date. If I access the item object using the memberKey now, and assign it to a variable, this new variable will be of type Date, and that's because TypeScript is smart enough to know that memberKey has a value of Date, and because the key date in the ReportItem is of type Date, the newly created variable is also of type Date. I won't do anything different in this case. So I'll just remove this code, and to finish it off, I'm going to call this validateRequired function from inside the isValid method. That's good for now for the keyof operator. During the next clips for this module, we'll see some other usages of the keyof operator.
Now let's take a look at what mapped types are, and how we can benefit from them. I'm going to create some space on this file because I'm going to experiment a little bit with some new types, and after we have a clear understanding of mapped types, we'll make some improvements in the demo code. So let's get to mapped types. Mapped types are types created based on other types. A mapped type is actually that simple. You have a type, let's say ReportItem is your type, and you will be creating a second type based on the first one, and you will map all the members of that first type, making the adjustments you need without changing the original type. Let's see an example. I have here the ReportItem type, and in this type, all members are required. Let's say I wanted to make a ReportItem where all members are optional. I'll call it ReportItem2. Then I'm going to use the special syntax for mapped properties. Inside the squared brackets, I'm going to say that for each property P in the keyof ReportItem, and I'm going to pause here for a second. What is P? P is just a name I gave. In this case, it stands for property, but you could name it whatever you wanted. It could be called X, or M, or whatever. The important part in this code is that I'm declaring that P is iterating over each key of the type ReportItem. We saw what keyof ReportItem means already in the previous clip, but in this context here, it's like we had a foreach loop and P would represent each member of the type ReportItem because the list of values are all the keys of ReportItem. Good. This is the left side of a mapped type. This means that this new type ReportItem2 will have all the same members of ReportItem with the same name. But what type each of those members will have? That's the right part of the expression. We'll add a column and then I'll say that the type is whatever type it was originally. This right part of the expression is saying get me the type from ReportItem for the current property P here. Keep in mind that I'm using P on the right side because I use it on the left side. So the name on both sides should be consistent, and that's it for mapped types. If you mouse over ReportItem2 you see that this type has all the same properties as ReportItem does with the same names and types. Now this doesn't make much sense right? Having two of the same types. So let's make some changes to ReportItem2. If you want an optional member on a TypeScript type, you just add the question mark after its name. The same goes here. If I add a question mark after the squared brackets, I'm making all the properties of ReportItem2 optional. Just mouse over ReportItem2 and you'll see it. See how every property now has question mark in front of it, you could easily now create a new object of type ReportItem2. You still have IntelliSense, but because all properties are now optional, you don't need to inform any of them. Now having the ability of creating optional types is very interesting, but in this example, we are restricting ourselves to just having a derivative type of ReportItem. What if I wanted to make this generic? Well, you could do that by saying that ReportItem2 is a generic type of T, and now you have to replace whenever ReportItem was used, by T; and of course, ReportItem2 is not now a very descript name. So I'll rename it to Optional. So what is this code doing? Well, whenever we use this type, we are creating a new type based on some original type T. That's the first line. On the left side of the expression, we are still telling TypeScript to iterate over each key of this new type T, and on the right side, we are telling TypeScript to use the type originally defined for the property of that type T. Now to use this new type Optional, let's recreate ReportItem 2, and now, I'm going to say that ReportItem2 is a type that is optional of type ReportItem, and again, if we mouse over ReportItem2, we'll see that every property of the type ReportItem is there, but it's now optional. And actually, this is so common that TypeScript comes with the special type called Partial that does exactly the same thing. It creates other types based on some type T with all its members optional. That's great, but making properties optional is not the only thing you can do. Let's say you wanted all properties from some source type, but instead of keeping the original type, you want them to be all strings. You could achieve that by changing the right sides of the expression to be of type string, and if I create a ReportItem3 based on this new type, we can see that all members now are of type string. Another use case is to make all members readonly. For that, all you have to do is add the readonly keyword before the square brackets, and just for fun, I'm going to let it also be optional. So now if I create an object using this type, if the member had a value when I created it, I can't change it. If I didn't have assigned value to a member, TypeScript considers that the member has a value of undefined, and I cannot change it either. Creating readonly types is also a very common task. So TypeScript has a special type for it called readonly. So if I wanted to create a new readonly type based on ReportItem, I could write the following. One more time, if you mouse over the new type, you can see the result. A new type with all properties of ReportItem, but now they are all readonly properties. One last special type TypeScript has that I want to talk about is pick. If for any reason I wanted to create a type derived from other type, but I don't want to use all of its properties, I can use pick. With pick, we can create a new type and it needs to know which type we are using as the source, and then we need to say which properties from the original type we'll want in this new type. For instance, if you wanted to create a new type based on ReportItem, we can choose to use date, amount, and description for the new type; and we have to separate each member we want to use with a pipe. If we make a typo, the code won't compile. Internally, TypeScript is using keyof to make sure we have the correct members from the source type. Now, if I mouse over the new type I just created, we can see that it has only the selected properties, and these properties have the same types as the original type. So we really have a subset of the original type. Good. So next, we'll be using pick and we will create a mapped type to fix a problem we saw in the previous module.
Refactoring Using Mapped Types
I have the ReportDataService file opened, and during the last module, we saw that the type Report has two members: id and amount, that we made optional because they were calculated when the new report was being saved, and we can see that here on the add method. Here's the id and the amount being calculated. This function expects an object of type Report as its only argument, and that's why you made those properties optional, so that when a new report is being created, we can pass in an object of the type Report without the id and amount. We also saw some consequences of that when we activated the strictNullChecks compiler option, we were forced to check if the id and amount properties had any value, if any cases we were sure there would be a value. One possible solution for this issue would be to expect each member of a report as an argument instead of an object of type Report, but that could lead to a lot of arguments being passed to a function, and to be honest, I don't like that solution very much. Report is not a big object, but if it was, imagine the problem of passing a dozen arguments to a function. Instead, I'm going to create a mapped type based on Report that does not contain id nor amount, and then I can make those two properties required again. I'm going to call this new type NewReport, because it represents a new report being created, and I'll be using pick to choose which members from report I want to use. Next, I'm going to update the add function to expect this new type, and where the add function was being called, I'm going to change it to pass the NewReport type, and of course, I have to import it; and now, the type Report is not used in this file anymore so I can remove it from the imports. With only these changes, I can go back to the Report type and update it so id and amount are not optional anymore. One downside of making these members non-optional is that the compiler won't tell us where there were checks for undefined in the code, which is one more reason to avoid optional parameters as much as we can. In this codebase, there aren't many places that need to be updated. There are a couple of places here on this file and also in the reports.component file, and in all cases, I was just using the exclamation mark, as we saw earlier. In a larger codebase, there could be a lot more checks for undefined, and using the correct types would prevent those.
Now, we're going to understand the real value of union types, and with the goal of having a real-world example, we're going to add some of our code through this demo. First, I just want to show you that I moved all types and interfaces that we have in this demo into a types.ts file. I like to do that so we avoid cyclical dependencies in the future and we have a central place for all types. This is good enough for a project this small. In larger projects, you may need to have the types in different files. Also, between the recording of the last module and this one, Angular material got updated from the release candidate version to a release version. This doesn't affect much of the code, but now I don't need to have a class that inherits from data service. I can just use the build team MatTableDataSource class. Besides that, the only change is that I have to subscribe to data changes from inside the constructor of the component, but this goal is the same as we had before. It's just in the constructor instead of being in the data service class, as we had previously. Now, in the constructor of the ReportDataService file, we are loading data that is stored in the localStorage. This would usually be a place where we would be loading data from a server in an HTTP request, and that's what we're going to simulate. In the end, we are still going to get the data from a local storage, but we will do that in an asynchronous way, just like one would normally do it. For that, we're going to add a fetch method to the report data class, and from the constructor, we are calling this new method. From the fetch method, we are going to simulate the async call using a set timeout. The code inside the set timeout will serialize the data from localStorage, and trigger a data change when the data is loaded with success. Also, I'm going to generate a random number, and if the number is less than or equal to three, I'm going to say that this code failed. I know that it's a 30% chance of failure. It's high, but this is a bad network and we want to see how to handle errors. Let's start by adding an if all statement here and if the random number is greater than three and data was found in the localStorage, we will parse the JSON data and trigger a data change. If not, we have to return an error somehow, and what if we also wanted to indicate that the data was being loaded during this one second or even more time it takes to get to the data? How would I do that? Should I use Boolean flags? A Boolean flag to say that there's an error and another one to say that there's a request in process? We could certainly do that, but then the burden would be on the developer to remember to always check for those flags, and what if someone forgets to do so? I think the best in this case is to use the compiler to let us be sure when we have the data, when we are loading the data, and so on. We're going to tackle this issue and have a code that is handling all these cases, and relying on the compiler to do so. First, let's open the types.ts file, and at the end of the file, I'll create a new type called RemoteData, and this is going to be our union type. The RemoteData type will be the union of four different cases. We can have a RemoteDataNotFetched, RemoteDataLoading, RemoteDataOK of T, and RemoteDataError. The pipe between each of these types, it's what makes RemoteData a union type. With this code, we are saying that an object of type RemoteData at any given moment, it's going to be either one of these. It can either be loading or be ok with some data T for instance, but it can never be both. Now I have to create these four types, and they are all going to have the same pattern. They will have a property named kind with a fixed value to identify each one of them, and for the ok and error cases, they also have some other properties: data with a type T when we have data, and error of type string when we have an error; and this is it for the types. Now, I'm going to write a few typeGuard functions to check for these types. If you don't know what a typeGuard function, once again, I'll recommend you to check the course Advanced TypeScript, here on Pluralsight. But essentially, a typeGuard function is a function that helps identify the type of a given value. For instance, this isRemoteDataOK is a function that returns true if an object of type RemoteData has its kind equals to OK, thus, we know it's of type OK. The special syntax for this function is it return type. The return type says that it will return true if the given argument called RemoteData is of type RemoteDataOK. Similarly, I'll create two other functions: isRemoteDataError and isRemoteDataLoading, because I know I'll be using those later. Back to the ReportDataService file. I'm going to change the type of the dataChange property. It's not going to be a behavior subject of an array of reports anymore. Instead, it's going to be of RemoteData of an array of reports. Now the dataChange is really representing the state of a RemoteData being loaded, and the first consequence is that I have to initialize it with the value of notFetched, which is true. When a new instance gets created, the data has not yet been fetched, and that's the correct state. On the fetch function, first I'll be updating the status and saying that data is being loaded, and for that, I'll pass an object of kind loading. Notice that because dataChange is of type RemoteData, we have auto complete for the kind property, and we can only pass the correct value, otherwise, we'll have an error. Finally, if this is a success, we have to update dataChange with an OK object, and the data, and if it's an error, with an error object and the error description. Now we have a few errors to fix on this file. First, on the getReport method, we can only look for the report if the data is loaded. So, I'll use the isRemoteDataOK typeGuard function for that. Inside of the if statement, the compiler knows that RemoteData is OK, and it has a property data, so it can resolve the promise. In the else case, I'll just return undefined, and we are safe here because we know by now that the client code for this function has to handle the undefined value. Now, you might say that we are now adding more code than we had before, and this is true. But this is a case where I think it's valid. We are making the code represent all possible states for this scenario and we are exposing possible bugs we were not handling before. For instance, the way this code is reading, when adding in a report, we have to check if the data is loaded before adding a report, otherwise, if the reports are loaded after, the added report will be lost. The same thing goes for the save toggleApproval and updateData functions. We need to make sure data is loaded before making changes to a report. Finally, on the reports.component file, we need to subscribe to the dataChange property, and check the state of this property. If it's okay, let's update the data for the table. If it's an error, let's just alert the error message. If it was loading, we could show a spinner for the user. I won't do that right now because this is more of a UI code than is relevant to this course, but the point is, in the client code of this RemoteData property, we are aware of the possible states of the code and we are guided by the compiler to handle each case respectively. It would be impossible to try and access the data from a RemoteData value if the data is not available, because a compiler only makes the data available when we checked for data, and that's the main goal of using union types: to make a type available only when it's the case and only when we had it checked by the compiler. This is one use case of union types, and I would argue that it's one of the most useful ones. Keep in mind, when you're writing your own code that every time you could have different states of the same type, maybe that's a good use case for a union types.
Exhaustive Check and Discriminated Unions
Remember during the first module when the noImplicitReturn option was activated and we had to update this function in the messenger.service file to throw an error so TypeScript would know we are handling all the cases? To recap, that was the case because the function signature says we have to return a messenger. So we can either do that or throw an error, and we decided to throw an error because the input for the function is a string, and being a string means we can pass any random string. And for the ones that this piece of code doesn't know how to handle, the function has to either return a default messenger or throw an error. So let's change that. First, I'll create a string literal type named MessengerEventsHandled, and in this type, we'll have all possible values for events. Then I'll update the getMessenger function to accept this new type instead of a string. With that, getMessenger gets type checked for its only argument and it's now impossible to pass a null expected string. Only these three values will be accepted. As soon as I change that, there's a new error. Where the getMessenger was being called, I was creating this string based on if the report was approved or not, and unfortunately, TypeScript cannot know the result of the string interpolation, so we have to be explicit about the value we'll be passing to getMessenger, and I will do that by creating a new constant with the event needed without using the string interpolation. This way, TypeScript knows we are using only valid values for getMessenger. Now back to the getMessenger function. If I remove the default case now, what will happen? Any guesses? We'll have no errors anymore, and why is that? Well, because now TypeScript knows all possible values of the event argument. It knows we have a case range for each possible one. If I add a new possible event to MessengerEventsHandled, then the function will yield an error again because now it's missing one possible case. Isn't that great? The fact that there is now a new possible case, we can rely on the compiler to tell us where we need to update the code. In fact, it's even better not to use the default case anymore because the default case would be acting as a catchall, and TypeScript would not know that a new case was added. In these situations, we want to know where are the impacted parts of the code, and using a default case would not allow that. This is what is called exhaustive check. TypeScript will check if we are exhausting all possible cases. In the same way, we can update the isValid function in the reportItem.service file. There is a switch case on the item.type, and because the item.type is not an open textbox, but rather a select box with limited values, we know there is no way to have a text that is not one of the provided options. So we can remove the default case and return an error in the case when a type was not selected. Now, if a new item.type is added, we would be reminded by the compiler to handle this new type in this function. I'll add officeSupplies as a new option to the item.type, and now, back to the isValid function, we see there's an error because we now have to handle it. I'll just handle officeSupplies like I would handle transport and travel; and if I want to make it available for the UI when creating a new component, I need to make officeSupplies a valid option on the respective component. The final example I want to show you is back on the reports.component file where we were handling the RemoteData object. I'll add a new function called handleRemoteData, and this function will receive an object of type remoteData of an array of reports, and the goal here is to use the compiler to tell us if we are handling all possible cases for this type because if at some point for whatever reason, someone adds a new subtype for the RemoteData type, we won't forget to handle it. We are handling RemoteData in the constructor with the help of typeGuard functions, and because of that, inside of each block the compiler knows the real type of RemoteData, as we saw previously, but every subtype of RemoteData share a common property, which is a property kind. Because of that, we can access it just by knowing that the type of the RemoteData argument is RemoteData. So I'll use a switch statement over remote.Data.kind. When I do that and I try to add the first case branch, we now have autocomplete to fill in valid strings, and once again, we can only use valid values for the kind property; otherwise, the code won't compile. I'll start handling the ok case, and I'm going to copy and paste the code we were using previously because that won't change, and for now, I'll use a break statement. When I mouse over remoteData inside of the case branch for the kind ok, we can see that this is of type remoteData ok because the data property is available. This code doesn't show the name of the type, but we know that the only subtype of remoteData that has a data property is remoteData ok, and also, on the mouse over, we can see that the kind property has the value of ok. Now let's do the same for the other two cases we have handled previously. Once again, mousing over remoteData inside the error branch displays a remoteData.error property. Now, we are missing one of the subtypes which is not fetched, and the compiler isn't yielding an error; and the reason for that is because the only way TypeScript can verify that is if the function returns a value, and it's not implicitly returning undefined. Although we don't have a value to be returned for this function, there is nothing stopping us from doing so. So what I will do is change the signature for the function to return a string, and the string I'll be returning is the value of kind from inside each switch statement. Of course, right now this code is not compiling because I have not returned anything yet, so let me fix that. After fixing the code for the existing case, we still have an error, and that's the exact error we should expect when we forget to handle all possible cases in this situation. Now, I'll add a case range for the not fetched type, and as soon as I do that, we'll have no more errors. Now I can remove the code from the constructor, and call this function. This piece of code here is what is called a discriminated union. We have a few different types that share a common type property. In this case, kind. Kind in a discriminated union is known as the discriminant. Next, we need a type alias to union disk types, which in this case is the remoteData type. That's our union type. Finally, we need the typeGuards on the common type properties, and those are the case branches. Because each kind property has a different value, this means that those are the identifiers and TypeScript can use them to identify each type individually. Discriminant unions are a great way to combine types together when they should be handled together in a way that the types can also carry their individual data like in the cases of remoteData.OK and remoteData.Error, or even when we just need to know when something happened and need to react to it in a special case, like for the RemoteData loading type.
This module was packed with information on how to better leverage the TypeScript compiler. I'm sure that now you have a lot of different ideas on how to use what we learned during this module to improve your code, how you can use keyof to have a typed access to a member of an object and even iterate over the members of an object without losing the type safety. You also know how to create derivative types using mapped types to avoid code duplication. A union type is also another tool for your toolbox, especially when you have types that are related to each other or represent different states, but must carry different data or transmit a different message. Finally, we saw how to use exhaustive checks and discriminated unions to rely on the compiler to tell us that we are not missing any special cases. I hope you had fun during this module. I certainly did. Now, let's move on to the next one, when we will learn a few techniques on how to avoid as much as we can to use the type any.
Avoiding the Any Type
Hello. My name is Thiago Temple, and welcome back to my course: Getting the Most from the TypeScript Compiler, and in this module: Avoiding the Any Type, we will learn a few strategies to avoid having to type any. If you are using TypeScript, there is a great chance one of the reasons for that is to have a static typed language and the benefits that come with such a language. When using the type any, we are losing the type safety that TypeScript provides, and although using the type any may be needed in some cases, we should try and avoid it as much as we can. During this module, we're going to investigate a few ways we can achieve that by augmenting existing interfaces when we are dealing with this external library. We are also going to see how to achieve the same goal, but using intersection types, and we'll take a look at a more advanced usage of generics in TypeScript. So let's begin.
Sometimes when working in an application, we need to change some of these external objects. Say, for instance the window object. The reason for doing so may vary, but it may be that we need to share data between frameworks because you're using different frameworks in your application. It may be to make it available for some code that is not using a framework and so on. The reason for that is not relevant for us here, neither it is if this is the best way to do that or not. The fact is that at some point, you may face this issue if you have not done so. I certainly did, and when you do that you evidently realize that the window object does not have the member you're trying to access. In our demo, let's say I wanted to save all the reports in a different member of the window object. A member called psExpenses. Of course, psExpenses is not available on the window object. You may be tempted to say that this window object is of type any, and if you do that of course, this code will work, but then you'll be facing the same traits of not using TypeScript to type your code. You cannot be sure you spelled psExpenses correctly or is psExpenses really of type string or is it an object? You know what I'm talking about. If we take a look at the definition of window, we can see that it's an interface defined on the lib.dom.d.ts file, which means it's part of the external library. So, instead of saying that the window object is of type any, what I'll do in this case is add a new file to the root folder of the project, and I'll call this file app.d.ts. I imagine you know what .d.ts files are, but just in case, these are files with only TypeScript type definitions. We cannot have functions or classes in such files, only type definitions. In this file, I will augment the window interface type, and to do that is pretty simple, especially because the window interface is available at the global level. I'm going to declare an interface with the name window, and when I do that, vs code shows that this interface is referenced 223 times, and that's because this is the same interface defined by this external library. Interfaces in TypeScript are complimentary, meaning that if they have the same name and are in the same scope or namespace, they will add to each other, and you can see that by the fact that I can declare the window interface a second time in the same file and TypeScript won't yield an error. The only rule you need to follow is to not have a member with conflict types. For instance, if I define a member on the first declaration with a type string, then I cannot redefine a member with the same name and a different type. If both had had the same type, it wouldn't be an issue, but then again, what is the reason for that? That is also valid for members to find in this external library. For instance, I know that there's a member called name of type string. If I decide to redefine in here with the type number, TypeScript will yield an error. Now, with this window interface declaration, I can add a member with the name psExpenses, and a type string. And now, if I go back to the previous code and remove the any, we won't have an error anymore, and of course, psExpenses is of type string just as we declared it. Many times when writing an
Many times when writing an application, we are using third-party libraries other than the main framework of choice. That is true not just for Angular, but for other frameworks as well. Sometimes we'll be lucky and have the type definitions for those libraries, some other times we may not be so lucky and end up using libraries that don't have a type definition. Imagine a hypothetical situation where we have a method that is expecting an event as an argument. The type event in this case, much as we saw with the type window in the previous clip, is part of the external library. Now let's say that this event is triggered by a third-party library that we don't have the type definitions for. In this hypothetical library, let's assume it's a library that manages drag and drop elements, the library adds drag and drop data to the event object. Now, because we don't have the tokens for this added data, we could say that the event is of type any, and deal with it, but as we saw earlier, this is not the optimal choice. Another option since the event type is part of the external library, would be to augment this type just like we did for the window type. So, to experiment a little bit with this idea, I'll open the app.d.ts file again, and let's just say that the data we're expecting is something like this. It will be an object with just an X and Y position, and will have that for our drag start and drag stop members. But doing so would add drag and drop members to a type that doesn't always has this data. Even in our case when the drag starts, the drag property would have a value, and when the drag stops, the drag start would be undefined and the drag stop would have a value. Then, the next thought would be to make these two properties optional, and although they are valid, I much prefer not to have optional values because we would need to check if they have any value whenever used. Instead, I'll get back to the method expecting the event, and I'll add an & symbol, and I'll say that the event argument will also contain a property named dragStart of type CustomDragEvent, and that's what the intersection type is. It's a combination of two or more types. In this case, we are telling to the compiler that the event argument is of type event, so it will have all members of an event, and it will also have a property of name dragStart with the informed type. Then, if I had a second method that handled the dragStop event, I could do the same thing, just changing the name of the property. Instead of dragStart, it would be dragStop. Now, in this case, I'm declaring the intersection of these two types inline on each method. This is good enough if they are only used in this file. If that's not the case, I'd create a new type for each one on the app.d.ts file. Here I'm creating two new types, DragStartEvent and DragStopEvent, that can now be used everywhere in the application. Now, you might be asking why use an intersection type and not inheritance? After all, this DragStartEvent type could be reading as an interface like this. In the two cases, the result is virtually the same with the main difference being that an interface can be inherited later on and the type cannot, but for me, and this is a personal choice of mine, I prefer to have type aliases to define my types per se, and by types I really mean a structure of an object that will carry data later on. Most of the times I don't use inheritance for these types. I generally compose them using this kind of technique, and I only create interfaces when I intend to create an abstraction that will have different implementations, just like one would use in an object oriented style; meaning that I would have different classes implementing that interface. It's a style that I find easier to identify what are structures or type definitions, and what are abstractions that need implementations.
The last topic I want to go over in this module is generics, and I'll start one more time saying that I won't go over the basics of generics. If you need to understand what a generic method class or interface is, I strongly recommend you watch Brice Wilson's course: TypeScript In-depth, here on Pluralsight, and pay special attention to the generic constraints part. In this module, we're going to see how we can use generics constraints in combination with some of the things we learned so far. Of course, generics are in essence, a way of having reusable code without losing type safety. Take this filter method for example. This function is very specific to a report array. If I wanted to filter another type of object, I can't, and if I wanted to filter using another property of report, I cannot do that either. Of course, this is a very simplistic example and it wouldn't be a problem to just call the filter method from an array with different arguments; and arrays in TypeScripts use generics as well, as we can see taking a peek in its type definition. But let's continue with this example so we can see how to dig deeper in the use of generics. Of course, the non-generic way of making this function non-dependent of the report type would be to change it to any, but that's not what we want to do. We want to keep the type safety, and I want to do this in a way that given an array of any type, and I'll use report as an example just because we have a report type defined, I want to filter it with a method called filterWith, and pass the name of the member from that type and a value to filter with, like so. I'm going to do this in a way that the name of the member will be checked by the compiler, and also the value that I'll be passing must be of the same type as the member I'm passing. In this example, I'm using a mount from the report type, so, the value passed to the function for filtering must be a number; otherwise, the code won't compile. Because I'm calling the function filterWith from an array instance, I have to add this method to the arrays prototype, and as soon as I try to do that, TypeScript says that this property does not exist in an array. So we know what we have to do now. We have to augment the standard array interface and add the method that we want. We could do that on the same app.d.ts file we used previously, but I'm going to do it in this file to keep both the type definition and the implementation code closed from each other, and in this way, we'll be able to make changes to both easily and learn something new. In the app.d.ts file, there are no imports nor exports, which means that this is known as an NBM file with only global definitions. The ReportDataService file has both imports and exports, and thus, it's considered a module and not an NBM file. To augment a global interface from within a module, we need to declare a special block: a global block, and inside of this block, we can augment the array interface. Now, inside of this block I'll declare the new function I want to create: filterWith, and for now, it will be a function that expects no arguments, and return an array of T, which will be the same type of the array instance because this T was defined on the interface. Next, I'll start writing the function, and I'll assign it to the array prototype, and because I'm assigning to the prototype, I have to re-declare that this function will be of some type T, and will return an array of the same type T, and I also say that inside of this function, the keyword this is of type array of T, just as we saw during the first module. The first argument that I want to pass to filterWith is a string that will be one of the keys of the type T. We saw how to do that during the second module using keyof, so we need some value, and I will make it a generic value, and I'll call it TKey, and TKey extends keyof T. With that, I'm saying that this TKey type must contain a value that is a string and that string must be keyof whatever the type T is. I'll call this first argument member, and its type is TKey, and I'll have to do that in both places: the interface and the function declaration. The second argument the function filterWith expects is a value that must be of the same type as the member selected on by the member argument. So I'll create a second generic type and I'll call it T value, and T value extends the type of the member selected and we do that in the same way we selected a type while creating mapped types. We say that TValue extends the type of the member from the type T with a key of type TKey, and I'll add a second argument to this function called value with the type TValue, and again, I have to do this in both places: the interface declaration and the function. Now we have everything we need in terms of arguments, and we can implement the function. I'll just call this.filter, and we filter each object by selecting the property using the key and comparing the given value. You can see that our test case is compiling now. There are no red lines under this method. If I delete amount and press Ctrl+Space, we can see that we have autocomplete to select a property from report, and if I select description for instance, 1, 2, and 3 is not a valid value anymore because it's a number and description is a string. Same thing if I select approved. Now I have to pass a Boolean value. I hope it's clear to you the power of using generic constraints from TypeScript. We can write really powerful generic methods, avoiding using the any type, and at the same time, having the type checks from the compiler all the way.
We have finished one more module, and we have seen some more advanced ways to avoid using the any type, and with that to keep our code statically typed. We now know how to extend existing interfaces and add methods and properties to them when needed. We also learned how to compose new types using intersection types when we don't want to extend the original types or interfaces. Finally, we saw how generics in TypeScript can be very powerful and flexible with the use of constraints and keyof. For the next module, we'll take a different approach. We're going to build a solution from scratch where we'll be using the compiler to avoid business errors in a way that the code won't even compile if the code is in a bad state.
Designing with Types
Hello. My name is Thiago Temple, and welcome back to my course: Getting the Most from the TypeScript Compiler, and in this module: Designing with Types, we are going to explore a different side of having a compiler. You see, at first we learn to rely on the compiler to check for the syntax of our applications so it can catch simple errors that we would otherwise forget, or would have to run the application to find those. Things like missing curly braces, parentheses, misspelled words, and so on. In this module, you'll see that we can also rely on the compiler to help us check the logic of our applications in a way that the code won't compile if we had any valid logic, and we are going to see that by writing code from scratch, and we will do that by using the Tennis Kata. If you've never heard the word Kata before, let me give you a brief explanation. A Kata is just some exercise with the goal of practicing. Usually Katas are complex enough so they will have meaning, but not too complex so the barrier of interest is too high, and that's the case for the Tennis Kata. If you want to, you can find more information about the Tennis Kata on this link. But here's the problem we're trying to solve. We're talking about the game of tennis, and specifically, we are talking about a game of tennis. In a tennis match, there are multiple sets, and each set is comprised of multiple games. In this exercise, we'll solve the problem of scoring one of these games. So let's understand the problem. In a game of tennis, a player can score points as Love, which represents 0, 15, 30, and 40. When a player has 40 points, if she scores again, she wins the game. When both players have 40 points, the game is in a state known as deuce, and when one of the players scores, she has the advantage. When a player has the advantage, if she scores, she wins the game. If the other player scores, the game becomes deuce again, and that's all there is to know about the problem we're trying to solve. In the next few clips, we are going to see how to build a solution for this problem using TypeScript and types. But before we do that, I have a disclaimer to make. The solution I'll be presenting is heavily inspired by a series of blog posts written by Mark Seemann, who's also an author here on Pluralsight. In his series of posts that you can read following this link, he talks about design with types using the F# language, and his whole solution for the Tennis Kata is proposed using F#. Here, I'm going to present a similar solution written in TypeScript of course, but inspired by his work. So let's begin writing some code.
The First Types
To begin, I have created a new folder for this project, and in this folder, I only have an index.ts file, where I'm going to start adding code. In the package.json file, we have only TypeScript installed, and on the tsconfig file, I have a pretty standard configuration file. The only relevant part in this file is that the options we talked about during the first module are activated. Let's begin writing a type that will hold the score for the game, and I'll call this type Score, and it will have two properties: points for player one, and points for player two. At first, this looks okay. After all, this type can hold our point values. We can say that love is 0, and of course, 15, 30, and 40 are numbers and can be stored in these properties. The problem is other numbers that we are not expecting can also be stored in an object of this type, as we can easily see. So that first attempt, although functional, is not what we are looking for. Let's try something different. Let me define a new type alias, and I'll call this type Point, and Point will be a string literal type with only the allowed point values in a tennis game. Now, I'll update the Score type and the points are not of type number anymore. Instead, they're of type Point. That's an improvement. We clearly cannot have invalid point numbers in here, and we also have the love points represented in the text form. Great. That's already better, and we've begun to rely on the compiler because nowhere on our code we would be able to assign invalid points to a player, but that's not all. There is still an issue with this type. We could certainly have a score where both players would have 40 points, and although this is clearly valid, this is not really representative of the state of the game. We know that this case that is known as deuce should be handled in a different way later on in the code. When a game is in a deuce state and a player scores, it's not the same as when a game has the score of 15/15, and a player scores. When it's deuce, we have to give the advantage to some player. When it's 15 to each player, we have to change the score to one of the players to 30, so let's represent that with types. First, I'll create a new type alias called Deuce, and all it has is a kind of deuce, so we can easily identify it. Next, I'll rename Score to PointsData, and I'll add a kind property as well with the value of points, and the third thing I'll do is create a new union type, and I'll call it Score, and score will be either deuce or PointsData. This looks pretty straightforward so far. Whenever we have an object of type Score, it can be either of type Deuce, which means both players have 40 points, and it can be in a state where players have different points. But because 40 is still part of the valid points, we still can have an object of type Points with both players having 40 points, and we don't want that. We want Deuce to represent that scenario. Let's fix that next.
A Player with Forty Points
To fix the issue of representing both players with 40 points, I'm going to remove the value 40 as one of the possible values for points. Now I'm sure I can't represent points data with two players having 40 points, but now I cannot represent any player having 40 points. There's no way for the score to be 40-30 or 40-love, so we need to fix that. I'm going to create a new type alias and I'll call it FortyData. It will have a kind property with the value forty, and it will have a property player representing which player has 40 points. Right now, this application doesn't have a way to identify players, and we need that for the new FortyData type. So let's create a new type Player, and we only have two players in this tennis match. Technically, we can have four players play in a match of tennis, but there are only ever two opponents, so we'll say a player is either PlayerOne or PlayerTwo. PlayerOne is a new type alias with a kind of playerOne, and a name, and PlayerTwo is a type alias with the kind playerTwo, and also a name. Now we can say that the type for the data has a property player of type Player. This will tell us that when we have an object of type FortyData, the value contained in the player property is the player with 40 points. Now we need to know how many points the other player has. So I'll add a new property called otherPlayerPoints of type point, and why is that? Well, because if we have a game with this score, that means that one player has 40 points; the player stored in the player property, and the other player has less than that. If both had 40 points, the game would have a score of deuce, and if none of the players had more than 30 points, the game would have the score of points. We can verify that by first adding the FortyData type as one of the possible score types, and then I'll open the second time here, just so we can make some extra invitations. First, I can say that the Score is Deuce. Next, I can say that the score is of type points, and one player has love, and the other has 30. But if I try to give a player 40 points, I won't be able to compile, and I also can say that a score can be of type forty, and I have to say which player is the one with 40 points, and I have to inform how many points the other player has. We are missing two other possible states for the score type, and they are very simple. One is the advantage state. To represent the advantage state, I'll create a new type alias called Advantage, with the kind advantage, and similarly to the FortyData, all we need to know is the player who has the advantage. So I'll add a property name player of type Player, and once again, Advantage is one of the possible states of score. The final type alias I'll create is to represent when a player has won the game. I'll call it Game with a kind of game, and it will also have a property player to represent the player who owned the game, and now add it to one of the possible scores. With that, we have all the types needed to represent the states of a tennis game. An object of type Score will never be ambiguous because it will always have a kind property to tell in each state the game is, and each state carries within itself all the data needed to represent the game, and even better, a given state cannot have invalid data, and we made sure of that by relying on the compiler to tell us which cases are valid. Next, we'll see how to go from one state to another.
Transitioning Between States
Now that we have all the types we need, we can start writing functions to update the score, and we will begin with the simplest of the cases. We are going to update the score when the game is deuce. That's pretty simple. When the game is deuce and the player scores, the game goes to advantage for the player who just scored. So let's write a function called scoreWhenDeuce that will take a player argument who's the player that just scored, and the function will return an Advantage type. And for the implementation of this function, we only need to return a new object with the kind advantage, and the player who has the advantage. Note that because I defined the function with the return type of advantage, the only kind I can return for the object is the kind advantage. That is checked by the compiler. Now we're going to write a function to transition from a state where a player has an advantage, and this function is called scoreWhenAdvantage, and the function will take this score that must be in a state of advantage, so of type Advantage, and a player who just scored, and what will this function return? It will return Game if the player who scored is also the player who has the advantage, or it will return Deuce if it was the other player who scored. Note that with this function signature, we are restricting the types that could be accepted and returned. There is no way to return or accept invalid states in this function. Now the implementation in this case is also simple. I'll just check if the player who scored is the same as the one who has the advantage, and if that's the case, I'll return an object of type game, because the player just won the game. If it was the other player, I'll just have to return an object of the type deuce. Now I'm going to write an auxiliary function to help me increment points. So far we know that points are love, 15, and 30, but we need a way to transition between them. So let's write a function called incrementPoint, and this function will just receive an argument of type Point, and it will return 15 or 30 because if you are incrementing points, we cannot return love because love is 0, and we can never increment to 0. The simplest way I can think of for handling this case is to write a switch statement and when the value of point is love, the function returns 15. When the value of point is 15, it returns 30. Now we still have to increment when the value of point is 30, and we are reminded by the compiler to do that, which is great. But what should we return when incrementing from 30? Is it 40? What if the other player has 40? Then the score should be in a deuce state and not have 40. Well, I want this function to just increment the points and be as simple as that. I'll have to decide what to do with that later, so I'll return undefined when point is equal to 30, and I have to add undefined as one of the possible return values for this function. I will deal with the undefined value later on. Next, we have to score when a player has 40 points, and I'll do that in a function called scoreWhenForty, which will receive the current score that must be of type FortyData, and the player who just scored. This function may return three different types. If the player who scored is the same player who has 40 points, the player just won the game, so we have to return Game. If the player who scored is not the player with 40 points, then there are two possible scenarios. Let's say the score for the game was 40-30, and the player who scored was the player with 30 points. Now the game is deuce, so we have to return Deuce. If the game was 40-15 for instance, and the player who scored was a player with 15 points, the game will still have a score of 40 data, we just have to increment the points for the other player, so the third type the function may return is FortyData. Now let's implement the function. The simplest case is to check if the player who scored is the same one that has 40 points. I'll do that in an if block, and if that's true, the function will return an object of type game with the player who just scored. If it was the other player, then we have to increment the other player points, and the function will do that by calling incrementPoint, passing the other player's current points. I'll store the result of that in a variable called newPoints, and this variable is either of type point or is undefined, and we are reminded by the compiler of that because the compiler will yield an error if I try to assign undefined to a variable of type Point. Now, if newPoints has the value of undefined, that means that the player who just scored had 30 points, which means that the score should now be deuce. If the value is not undefined, that means that the player had less than 30 points, and we just need to update the current score with the new points for the player. Next, we'll transition when both players have 30 or less points, and tie everything up.
Tying Everything Up
We're almost there. We have a few more functions to write, and we'll be done. First, I'll write a small function that will update a score of type PointsData to another score of PointsData. Let's say the game is just beginning, and the score is love to love. So it's of type PointsData. When a player scores, we'll have to update the score of the current player, but the result will still be a score of PointsData. So I'll add a function updatePointsData that expects a score that must be of type PointsData, a newPoints argument, and the player who scored the point, and the only thing this function can return is an object of type PointsData. Again, I'll use a switch statement on a player.kind property. If the value of kind is playerOne, the function will return the same score object with just the points for the PlayerOne updated. If the value of kind is playerTwo, the PlayerTwo points will be updated and returned. As a reminder from the previous module, this is again a discriminated union, where the property kind is a discriminant, player is the union, and the switch case is acting as the typeGuard. So we are required once again to handle all possible cases and return the specified value on the signature of the function. Now we have to handle the case where a player may have 30 points and score, and it will have then 40 points. So let's write a new function: createFortyData, that will do just that. This function will expect the current score that must be of types PointsData, and the player who just scored, and it will return an object of type FortyData. We know that the player who just scored is the player who now has 40 points. Now we need to retrieve the points for the other player, and we'll do that by checking which player scored, and getting the other player points from the score, and the function can now return an object of type FortyData with the player who has 40 points, and the points for the other player. These two functions will be used when we have to score on the last case, which is when the score is on the PointsData state. So, I'll create a function called scoreWhenPoints, which will receive a score of type PointsData, and a player who scores, and this function can only have two possible outcomes. One is if the player who just scored does not have met 40 points yet. In this case, the function will return another object of type PointsData with the updated score. The second possible case is when the player who just scored has now 40 points, in which case the function has returned an object of type FortyData. The first thing we need to find out is exactly that. How many points the player who has scored has, and we'll do that by calling the increment Point function we created previously. I'll create a new constant called newPoints of type Point or undefined, and if the player who scored is the PlayerOne, I'll increment her points. If it's not, I'll increment the points for PlayerTwo. If newPoints is undefined, that means this player now has 40 points, and we have to return an object of type FortyData. If newPoints is defined and has a value, we'll have to return an object of type PointsData with the updated points. That's it for this function. Now we have to wire everything up, and for that, I'll create a new function called score, and score takes the current score as an argument, and a player who has scored, and it will return a score as a result. One more time, we're going to use a discriminated union to implement this function. Using a switch case over the property kind from score, if the property is equal to points, then we'll return the result of calling scoreWhenPoints. Next, we'll do the same for when kind is equal forty, and return the result of calling scoreWhenForty, and of course, we have to do the same thing for deuce and advantage. Now the compiler is yielding an error because the function is not returning a score for all cases. I forgot to handle the case where kind is equal to game. Fortunately, this is an easy case to handle. We should not have to handle a score when it's already won by a player, so we could throw an error here, but I'm just going to return the same score object because a game won by a player should not have its result changed.
This is all the code I had planned for this Kata. I'll leave to you to write code for a UI to this game. You could make a web page or a node application. It's your choice. Well let's take a moment to review this code. We have created a score type alias that represents all possible states of the application, and because of this, whenever a new object of type Score is created, we know the exact state of the application, and we have the whole data needed to represent it. To represent each possible state of a score, we have a different type. Starting with the PointsData type, we have limited the values accepted by as a point, and the compiler will not accept invalid values, and we cannot represent any valid state using this type. If a player has 40 points, has the advantage, or the game is deuce or won by a player, we have a type to represent each of these cases. Next, we have functions to transition from one state to another, and on each function signature, we have limited the type of score that the function has to accept to be called; and also what are your possible states a function can return. So it's impossible to call a function in a bad state. One exception to this rule is scoreWhenDeuce. This function is not accepting any score object because it doesn't need one to calculate the new score, but we could accept one of type Deuce, just to make sure that the function cannot be called when a score is not on a deuce state. And finally, we have a score function which is the one responsible for checking the state of the current score, and dispatch the score and the player to the correct function, returning the new score as a result.
This course is coming to an end, and I hope you have learned some new things with it. Let's just take a moment and do a quick recap of all we saw during the four modules of this course. We began by activating options in the tsconfig file so that the compiler will check more things in our code. In a new codebase, I would always start with those activated. In an existing codebase, you may need to update the existing code when they are activated, but if possible, you should do it. These changes will help you avoid mistakes that aren't easily detected by the compiler. During the second module, we saw some of the more advanced features of the language. This may be new to you if you just started working with TypeScript, or maybe something that you knew of but never had the chance to put it to practice. I hope that you saw the value of using them. Having a statically typed code is one of the goals of using TypeScript. During the third module, we saw a few ways to avoid using the any type, and a special case using generics, putting some of what we learned during the second module to use. In the final module, we walked through an example of how we can use types to have a more strongly typed and well-designed application. Try to keep that in mind the next time you are writing your own types. Having the feedback from the compiler is faster than needing to run the application to test it; and with that, I want to finish, hoping that you now have a deeper understanding on how you can use the TypeScript language and its compiler in writing your applications. Thank you once again for watching this course. If you have any questions, you can reach me on the discussion section here on Pluralsight, or using Twitter.
Thiago Temple is a Brazilian developer based in Ottawa, Canada. He has been building software for more than fifteen years, and his professional interests are mostly related to web development both...
Released1 Feb 2018