What do you want to learn?
Skip to main content
Getting Started with TypeScript
by Brice Wilson
TypeScript is a powerful, fun, and popular programming language used for building browser and NodeJS applications. This course will teach you all of the most important features of TypeScript, and quickly make you productive with the language.
Start CourseBookmarkAdd to Channel
Table of contents
Installing TypeScript and Configuring a Project
Demo: Installing TypeScript and Running the Compiler
Using Project Files
Demo: Configuring Compiler Options in tsconfig.json
Demo: Configuration Inheritance and Glob Support in tsconfig.json
Demo: Compiling with a Visual Studio Code Build Task
In this demo I'll show you how to set up the Visual Studio Code build task that I'll use throughout the rest of the course to start the TypeScript compiler. This is just a quick, handy tip for Visual Studio code users. Setting up a build task can save you a lot of keystrokes and, in this course, it will save you from having to watch me manually jump into a terminal and start the compiler so often. I'll open the command pallet and type the word task to search for the commands related to task. I'll choose the first option, Configure Task Runner. I now have to specify what kind of task runner I want to use. Since my project is configured to use TypeScript project files, I'll select the TypeScript tsconfig.json option. That creates a new file in the .vscode folder named tasks.json. The task will effectively just run the TypeScript compiler as a shell command for me. The command it will run is tsc. It's currently configured to be past the -p compiler option. That's the shorthand notation for the --project option that lets you specify the exact location of the tsconfig file you want the compiler to use. For the sake of clarity, I'll change the shorthand notation to --project, and let it know the tsconfig file it should use is in the app folder. I want to see the results of the command running, so I'll change the showOutput option from silent to always. I'll now execute this task from the command pallet. I'll search for build, and then select Run Build Task. Note that I can also start it with the keyboard shortcut, Ctrl+Shift+B. I'll probably most often use that for the remainder of the course. Once the task starts the output window in Visual Studio Code opens, and we can see that the compilation was complete and that the compiler is watching for changes to the source files. There's also an integrated terminal that can save me the trouble of switching apps, just to run the npm script that starts the web server. I'll open it and run the command npm start. It successfully started the web server, and I can now jump over to a browser, and access the app by going to localhost:8080. That's generally the workflow I'll use throughout the course to start the compiler and web server before checking out our code changes in a browser.
Taking Advantage of Built-in Types
Introduction and Overview
Basic Types and Variable Declarations
Type Annotations and Type Inference
Type annotations are another important part of variable declarations that I didn't include on the last slide. They're a way of specifying what data type must be assigned to the variable. Here I'm declaring a new string variable named x. I've annotated the variable with the string type. Annotations are added by placing a colon after the variable, followed by the type you want the variable to store. Now that I've declared this variable as a string that's the only type I can ever assign to it. If I were to assign a different type to it, like a number, I'll get a compiler error. I want to point out that you're not required to add type annotations to your declarations. Here I've declared a new variable and also initialized it with a string. I named it y and left off the type annotation. Even though I didn't provide an annotation the variable still has a specific type. The compiler infers the type based on the value I used to initialize the variable. Therefore, this variable will also be a string. If I try to assign a number to it I'll get a compiler error. The TypeScript compiler is very good at inferring the type of variables, so whether you choose to use annotations is somewhat a matter of coding style. However, I want to make the case that you should use them for the sake of clarity, and to make it easier to read for the developers that may be reading your code in the future, which may even include yourself. It's easy enough to glance at the two variables I've declared here and immediately know their strings, but what if I declare a new variable and assign it the return value of some function? It may not be immediately obvious what data type the compiler will infer from my z variable here. In many editors you can hover over the variable or the function and find out what types are assigned or returned, but even though this is perfectly valid syntax I prefer to be more explicit and use type annotations. I personally like to see the type right there with the declaration. Even though I recommend using type annotations, I encourage you to decide on a standard format for variable declarations with your team and be consistent, no matter which format you decide to use. Okay, let's now go experiment with some of these techniques in the demo app.
Demo: Using let and const with Type Annotations
Additional Built-in Types
Union Types and the --strictNullChecks Compiler Option
Sometimes, for one reason or another, you might know more about the type of a variable than the compiler does. Maybe you're using a third party library that uses dynamic types or you have an object literal that was never assigned a specific type. If you find yourself in this situation you can effectively assign it the correct type with a type assertion. For example, here I've got a variable named value that I declared with the any type. Remember that the any type just means that any value can be assigned to the variable. I'm initializing this variable to the number five. I could later assert that the variable should actually have the number type. Doing so would let me take advantage of all of the TypeScript related benefits you've already seen, such as code completion in my editor, type checking, and things like that. The syntax for asserting a type is to place the type inside angle brackets in front of the variable. Here I'm asserting that the value variable is a number. I've wrapped the assertion in parentheses, so that I can then call a number method immediately after I've asserted that as its type. The toFixed method returns a string, which I can then just log to the console. There's also an alternative assertion syntax that does the exact same thing. You can also use the as keyword after the variable, and then specify the type it should be. Here I've wrapped value as number in parentheses, just as I did for the other assertion above, so I can call a method on the resulting number. The result is the same. Which syntax you choose to use is really just a matter of which one you prefer. Let's now jump into a demo, and turn on the strictNullChecks compiler option, and see how to use another type of assertion that's a little different than those I've shown you here.
Demo: Writing Better Code with the --strictNullChecks Option
Demo: Understanding Control Flow-based Type Analysis
So far in this module you've seen some of the benefits of the TypeScript compiler strong type checking. In this last demo I want to show you one of its neatest tricks, known as control flow-based type analysis. This type of analysis is the compiler analyzing the conditional behavior in your code, and applying type checking to your variables using the most narrow type possible in any given code branch. Let's look at an example. I've declared the messagesElement variable here to be a union type of HTMLElement and null. Right after that I'm going to add an if block just to illustrate the analysis performed by the compiler. For the condition I'll check to see if messagesElement is null. Inside the block I'll just return the variable. It's not important what I have it do, but I'll hover over the variable inside the block, and you can see that TypeScript no longer reports it as a union type. Instead its type is just null. That's because based on the condition I used the type can only be null at this point inside the if block. The TypeScript compiler recognized that and knows that HTMLElement is no longer a valid type for the variable right here. I'll add an else block, and just put another meaningless reference to the variable inside it. In this case, we know the variable is not null, so if I hover over it inside this block the compiler reports its type as HTMLElement. It's no longer a union type that includes null since it can't possibly be null at this point. The compiler has narrowed it to the most specific type possible, however, I can re-widen the type to any of the types specified in its initial union type declaration. To do that I'll copy the call to getElementById I used to initialize it, and reassign that to it in the else block. I'll hover over this new assignment, and TypeScript again reports that it can be either an HTML element or null. I'll again, hover over the line right above this, and at that point it can still only be an HTMLElement. I think it's pretty impressive that the compiler can analyze your code this way and always type check against the most specific type possible. Do note that you can't widen a variable beyond the types in its initial type declaration. If I try to assign a number to the messagesElement variable I get an error. I'm still constrained by the initial declaration.
Writing Better Functions with TypeScript
Introduction and Overview
Adding Type Annotations to Functions
Using the --noImplicitAny Compiler Option
On the previous slide I showed you this dull function and mentioned that without type annotations the two function parameters would be implicitly assigned the any type. Using the any type implicitly or explicitly effectively turns off type checking for that variable or parameter. If you want to avoid accidentally using an implicit any type, then you can use the noImplicitAny compiler option. You can still explicitly annotate a variable with the any type using this option, but if you have any implicit any's then you'll get errors like these letting me know the two parameters for my dull function implicitly have the any type. This is a helpful and firm reminder from the compiler that I forgot to add type annotations to my parameter.
The last trick I wanted to show you related to parameters are default-initialized parameters. Here I've got a simple function named sendGreeting that takes one parameter named greeting. I've annotated it as a string, but I've also added an equal after the annotation, and then assigned it the default value, Good morning. This effectively makes the parameter optional and assigns it the default value if no value is passed to the function. Also note that since this function doesn't return a value I've specified void as its return type. With the default parameter in place I can call the function without passing in a parameter, and it will print out Good morning. However, if I call it with a specific value, like Good afternoon, then it will assign that to the parameter and not use the default value. Both of these are perfectly valid. Careful use of default-initialized parameters gives you the flexibility to call a function without passing every piece of data it may need, but they can also smooth over accidentally leaving off a parameter here and there. Let's now go take advantage of some of these techniques in the demo app.
Demo: Adding Type Annotations and Default Parameter Values
In this demo I'll add type annotations and some default parameter values to the functions in the MultiMath app. Before we get started writing any code I'm going to open the tsconfig.base.json file here at the root of my project, and change one of my compiler options. I already have the noImplicitAny option listed here, but it's currently set to false. I'm going to change it to true, so that the compiler will alert me whenever it has to implicitly assign the type any to a variable. I'll now press Ctrl+Shift+B to start my build task and compile all of my code with this option enabled. Okay, changing that option only exposed one place in the code I need to work on. On line 12 of app.ts the compiler is telling me that parameter 'name' implicitly has an any type. I'll close all of this stuff, and then open the code and show you how we can fix that. Visual Studio Code is again, making it easy for me to spot the error with the red squiggly under the name parameter on the logPlayer function. If I hover over it we see the same error that I got from the compiler output in the terminal. This was pretty easy to fix. I know this parameter should really be a string, so I'll just add a colon in the string type after the parameter name to properly annotate it. Now the compiler is happy again. No more red squiggly. While I'm here I'll also add a type annotation for the function's return value. It doesn't return anything, so I'll just add a colon and the void keyword after the parameter list. Okay, I now want to write a couple of new functions that will help me capture user input in the app, and display the user scores on the screen. Before I start writing them I want to quickly jump back to the index.html file and point out a couple of things that I'm going to be taking advantage of in the code. The first thing I want to point out here is the input box prompting the user to enter their name. There's nothing special about it, it's just a regular text input box, and I gave it the id playername. I'm going to write a function that will let me pass in the id of an input box, and get back the value in the box. The other thing I need to do in this file is give myself a place to output the scores. I'll jump down to the end of this file. I've already got a div element here where I want to display the scores, but I'm going to add an id value to the h4 element, so I can easily reference and add the new scores to it. I'll call it postedScores. Now I'm ready to write some new code. I'll go back to app.ts and create a new function named getInputValue. I'll pass it the id of one of the HTML input elements, and I'll annotation that parameter with the string data type. If the function finds a value in the HTML element it will return a string. Otherwise, I'm going to have it return undefined, so I'm using this union type as the return type for the function. I'll use a code snippet to paste in the body of the function. The first thing this code does is call the getElementById function with the id that was passed to the function. I showed you in the last module that this function normally returns a value with the type HTMLElement; however, in this case, I know I'm going to pass it ids of elements that are actual input elements, so I'm adding a type assertion that will treat the element it finds as the more specific HTMLInputElement type. I'm assigning the found element to a variable named inputElement. The HTMLInputElement type has a property on it named value that lets you retrieve the value in the inputElement. If the value of the element is an empty string, then this function will return undefined; otherwise, it will return the value the user provided. Okay, so there's a nice, new, fully annotated function that's going to make it much easier for me to retrieve user input values. Let's now write another one to post scores to the screen. I'll name this one postScore, and it will take number and string parameters for the score and name of the player that received the score. It won't return a value, so I'll give it the void return type. I use another code snippet for the body. I'm first getting a reference to the element with the posted scores id I showed you in the index.html file. I then just print the score and the player name to the screen by assigning a template string to the innerText property of the HTMLElement. Note that I'm using the nonNull assertion operator I showed you in the last module to assert that the posted scores element won't be null. Okay, I'm now going to come back up here to this startGame function, and write just a few lines of code to test out the new functions. I don't have any code to actually play a game yet, but I want to call the new functions, just to see if they work as expected. I'm first going to delete these last few lines that just displayed a couple of welcome messages. I'll now call the new postScore function and pass it 100 as the score and the value and the playerName variable I set above. Since I now have a function to retrieve values from input boxes I'm going to remove the hard coded playerName and replace it with a call to the getInputValue function. I'll pass it the playername id I showed you in the index.html file. That's now giving me an error because the return type of the function is the union of string and undefined. I'll fix that by changing the type of the variable to match the function. That cleared up that error, but I now have two new ones just below it when I call logPlayer and postScore. The problem is that both of those functions are expecting strings, but the type of the variable I am passing to them is a string or undefined. I'm going to fix that on the postScore function by changing the function to make the playerName parameter optional. I do that by just adding a question mark after the parameter name. Making it optional means it's now okay to pass it undefined. I'm going to fix the logPlayer function a little differently. I'm going to assign a default value to the name parameter. I'll set it to MultiMath Player. Remember that using a default initialized parameter also makes that parameter optional, which means that passing in undefined is now perfectly valid, and the error on the function call goes away. Let's now run this code and see how it actually behaves. I'll open the terminal in Visual Studio Code and start the web server with the command, npm start. I'll then jump over to my browser and go to localhost:8080. I want to see the output in the console, so I'll open the developer tools. I'll type a name in the Player Name box and click the Start Game button. Everything looks good so far. The hard coded score with the correct player name appears on the screen, and the log player function logged the correct value in the console. I'm now going to remove the name from the Player Name box and click the Start Game button again. Because of the way I wrote the getInputValue function this is effectively causing undefined to be passed to the logPlayer and postScore functions. You can see in the console that the logPlayer function used the default parameter value I specified in the function definition. However, the behavior of the postScore function was not as nice. I didn't get any errors, but printing undefined to the screen is not really what I want. Let's go back to the code and fix that. Just to demonstrate that passing undefined is really the same thing as passing nothing, I'm going to change the call to postScore to not pass the second parameter at all. I'll also change the score I'm passing, just so we can see that it's different. I'll then change the function itself to use a default parameter like the logPlayer function. I'll remove the question mark, making the parameter optional, and then add the same default value I used with logPlayer. Remember that adding a default value also makes the parameter optional. I'll hop back over to my browser and refresh the page. I'm going to leave the Player Name box blank and just click the Start Game button. This time we see that both functions use the default parameter values. Do note that those default values were applied under slightly different circumstances. The logPlayer function was passed a value, but the value was undefined. The parameter was completely omitted from the call to postScore, but in both cases a default value was applied. I'll quickly go back to the code and add the playerName variable back to the call to postScore. Okay, let's now go talk about arrow functions.
Anatomy of an Arrow Function
Demo: Converting a Traditional Function to an Arrow Function
Demo: Taking Advantage of Function Types
In this quick demo I'll demonstrate how TypeScript functions have a type of their own and how you can take advantage of that in your code. The first thing I'm going to do is delete this call to logMessage I added in the last demo. However, I'm going to leave the function itself. I'll then write a new function named logError that takes a string parameter and uses the error function on the console object to report the error passed to the function. This new function, like all TypeScript functions, has a type of its own that's defined by the type of all of the function parameters and the function's return type. I'll hover over the function name, and a little pop-up shows me the function type. It takes a string parameter and returns void. It just so happens, that is the same type as the logMessage arrow function I wrote in the last demo. I'll hover over it, and you can see that it also takes a string parameter and returns void. What this means is that if I declare a variable to have that same function type I could assign either of these functions to that variable. Let's give it a try in the postScore function. The first thing I'm going to do is declare a new variable named logger. You can use function types as type annotations just like you use string, number or any other type. To give this variable a function type I just add a colon after the variable name as usual, and then put the expected function parameters and their types in parentheses, followed by an arrow and the expected function return type. This logger variable may now be assigned any function that takes a single string parameter and returns void. Just like the logMessage and logError functions I wrote earlier. I'll now add an if block to check the value of the score parameter. If the score passed in is less than 0, then I'm going to assign a logError function to the logger variable, since I want this condition reported as an error. Otherwise, I'll assign the logMessage function to the variable. At the end of this function I'll then use the logger variable to call whichever function got assigned to it. I'll just pass it a template string that will output the score. So that we can see an example of each condition I'm going to add one more call to postScore above in the startGame function, and pass it -5 as the score. I'll go back to my browser and refresh again. I'll quickly add a player name, and then click the Start Game button. The first call to postScore used a score value greater than 0, which caused the logMessage function to be called. You can see the result here in the console. The second call to postScore used a negative score, which caused the logError function to be called. It called the error function on the console object, which caused the output to appear as an error here in the console. This was all possible because of the fact that the logMessage and logError functions have the same function type.
Creating and Using Custom Types
Introduction and Overview
Hi. This is Brice Wilson. So far in this course I've shown you lots of ways to use the built-in TypeScript types that help you write better code with fewer errors. It turns out that you can get all of the same great type checking support with your own custom types as well. Object oriented developers should feel right at home with the topics in this module, but don't worry if it's all new to you. I'll cover everything you need to know. Creating your own custom types is really all about two TypeScript language features, interfaces and classes. It's only two features, but they're really big features. I'll first explain the differences between the two and when you might use one over the other. I'll then get into the details and show you how to create and use them in your apps. Along the way we'll take a brief detour as I show you how to configure TypeScript projects to support multiple source files, but let's start by looking at the differences in classes and interfaces.
Interfaces vs. Classes
Interfaces and classes can both be used to create custom types in TypeScript. They're similar, but also different from each other in a few very significant ways. First of all, interfaces and classes are both used to define new types. Interfaces have properties, but they just define the signature of the property. What this really means is that the property only has a name and a type, which is often all they need. Classes also have properties, but unlike interfaces they can also provide implementation details for the property. This usually takes the form of custom accessor functions. Experienced object oriented developers will know these as getters and setters. Interfaces and classes also both define methods. Similar to the properties they define, interface methods are just a method signature. They define the number and type of parameters the method expects, as well as the method's return type; however, they don't provide any implementation. That has to be done by an object that implements the interface. I'll show you how to do that a little later. Class methods do include full method implementations and each instance of the class will get the same code for a given method. The final difference between interfaces and classes that I want to highlight here is that interfaces can't be instantiated. They effectively define a contract. If you have an object that you know implements a given interface, then you know the object will have all of the properties and methods defined on the interface; however, you can't use the interface to actually create an object with those properties and methods. Classes can be instantiated. You can use the new keyword I'll show you shortly to create new instances of a class. Each new instance is an object that has all of the properties and methods defined on the class. I think a useful analogy to use when trying to understand the differences between interfaces and classes is that of building a house for yourself. When you first start thinking about the kind of house you want you'll probably think about it in terms of the features you want. Maybe you know you want three bedrooms, two bathrooms, and a front porch. This is a high level abstraction of the house you'll ultimately end up with. You're defining some of the properties of your future house, but not the details, such as total square footage or the dimensions of each bedroom. This abstract house you have in mind is very much like an interface. It has the properties and methods sketched out, but you don't have all the details. Deciding on a specific house includes all of the details that were lacking when you first imagined the house you wanted. Specific houses or plans for houses, like this one here, have precise dimensions, and you know exactly what you'll get when the house is built. Just like a class, it includes all of the implementation details, so that you know exactly what the finished product will look like. Interfaces are useful when you only need to describe the shape of an object without all of those details. Maybe you'll want to implement a particular interface differently in different parts of your application. Interfaces are also great if the object you need to describe doesn't have any behaviors, usually implemented as methods. If you just need a list of properties that define some particular type of object, then an interface is perfect. Let's now see how to create one.
Creating an Interface
I'm first going to create a very simple interface for an Employee. Interfaces are defined by using the interface keyword followed by the name you want to give the interface. The body of the interface is surrounded by curly braces. I'm going to give this interface two string properties, name and title. Any object that implements this interface must have those two properties with those two data types. Let's now look at a slightly more complex example. Here I've defined a Manager interface. I've used the extends keyword to specify that this interface will extend the Employee interface above. That means that this interface will inherit all of the members defined on the interface it extends, so already we know this new interface will have name and title properties. I'm going to add to that a property for the manager's department and the number of employees he or she manages. Even though they're not all explicitly listed here, we know this interface now has four properties. I'll also add one method to it. Remember that methods on interfaces don't actually provide any implementation, just a signature. The signature is defined by assigning a function type to the method like I showed you in the last module. The type of this scheduleMeeting method is a function that takes one string parameter and returns void. It would be up to an object that implements this interface to provide the actual code that executes when the scheduleMeeting method is called. Because they don't provide any implementation details you'll often hear that interfaces only define the shape of an object. The shape of an object is very important when working in a language that uses a structural type system like TypeScript. Let's talk about that a little more next.
TypeScript's Structural Type System
Structural type systems, like the one used by TypeScript, use the structure of objects to determine their compatibility. For instance, let's look again, at the Employee interface I showed you on the last slide. It has two properties, name and title. Let's now suppose I declare a new variable and just assign it an object literal. This object has three properties. The first two match the property names and types to find on the employee interface. Because it has all of the required properties this object implements the Employee interface and may be used anywhere an employee object is expected, even though I haven't done anything to explicitly declare that it represents an employee. The third property on the object literal doesn't exist on the interface, but that doesn't mean it doesn't still implement the interface. As long as it has all of the required members, the interface is implemented, and the object can have whatever additional members it needs. Therefore, I can declare a new variable to have the Employee type, and and a sign of the object literal that implements the interface, and I'll get no complaints from the compiler. All of this is possible because TypeScript implements a structural type system. As long as the structures match, then you can treat the object as the type with that structure, even if it wasn't explicitly declared with that type. This is often referred to as duck typing. If something walks like a duck, swims like a duck, and quacks like a duck, then it must be a duck. Let's now add some interfaces to the demo app.
Demo: Creating Interfaces
Let's take a look at class members and some of the features they support that don't exist on interfaces. Probably the most obvious example of that is method implementations. Classes include complete function definitions for the methods on the class. They also include property implementations; however, these will often look just like the non-implemented properties on interfaces, since many properties are nothing more than a property name and a data type. However, TypeScript does support property accessor functions, which let you write custom code to define how a property is set or retrieved. Accessor functions are often referred to as getters and setters and definitely provide property implementations that can't exist on interfaces. Class members can also include one of a number of access modifiers that control the accessibility of the member. By default, all members on TypeScript classes are public, but there is also a public keyword you can use before a member name if you prefer to be more explicit. Members may also be declared as private if you don't want it to be accessible outside the class or protected if it may only be accessed inside the class or any classes that inherit from the class. Let's look at an example. I'm going to define a new class named Developer. I just used the class keyword followed by the name I want to give the class. The body of the class goes inside a pair of curly braces. I'll first give my new class a public string property named department. Remember that class members are public by default, so I don't need to prefix this declaration with the public keyword. Next, I'll declare a private member named title. I like to prefix all private member names with an underscore as a quick visual clue that the member is not public. In this case, I did use an access modifier, placing the private keyword in front of the declaration. My plan is to use this private member as a backing variable for a pair of accessor methods, so I'll add them next. Accessor methods allow you to customize how you get and set data for a property. In this example I'm using accessor methods for a property named title. The syntax for accessors is pretty much like any other function with the exception of the get and set keywords. Here I've placed the get keyword in front of the function that will return the value for the property. The name of the function is the name I want to give the property. The getter shouldn't take any parameters and should return the type of the property. In this example I'm returning the value in the _title variable. The setter function is prefixed with the set keyword, and is also given the name you want to use for the property. It should take one parameter, which is the value being assigned to the property. Here I'm taking the value passed in and converting it to uppercase before storing it in the backing variable. If I wanted to make this property read-only I would just write the getter and not implement a setter. One important piece that I want to point out here is my use of the this keyword. Whenever you want to refer to a class member that belongs to the class you're in you need to prefix the reference to the member with this followed by a period. I will admit this can take some getting used to. I also write a lot of C#, which doesn't require this extra reference. Jumping between it and TypeScript still occasionally leaves me scratching my head when I get a TypeScript error, only to realize I forgot to include this in front of a class member. Class methods are nearly identical to any other function you might write. The one exception is that you don't include the function keyword in front of the function definition.
Extending Classes and Implementing Interfaces
I showed you earlier how you can extend an interface. You can do the same thing with classes. Here I'm using the extends keyword to specify that the WebDeveloper class extends the developer class. This creates a very typical object oriented inheritance relationship between the two classes. The developer class on the previous slide had department and title properties, as well as a document requirements method. This class inherits those members and adds a favoriteEditor property and a writeTypeScript method. I can create an instance of this class by declaring a variable and using the new keyword followed by the name of the class to call the classes constructor and create a new instance. I'll talk more about constructors in just a little bit. I can use the new instance to set properties defined on the base Developer class or the WebDeveloper class. I mentioned earlier that interfaces don't provide any implementation details. It's up to objects that implement the interface to provide those details. A common practice is to design classes to implement interfaces. Here I've got an employee interface that has a couple of properties and a method named logID that takes no parameters and returns a string. I'll now add a new class named Engineer that implements that interface. I declare that this class will implement the interface using the implements keyword right after the class name. That's followed by the name of the interface it will implement. Once you've declared that a class will implement an interface you're required to provide an implementation for all the required properties and methods on the interface. A basic implementation of the two properties looks just like the property definitions on the interface, although I could have used accessors if I needed to implement some custom logic. The function requires a little more code, since I need to provide a function body that returns the expected string type. Let's now go add some classes to the MultiMath app.
Demo: Creating Classes
Demo: Configuring a Project with Multiple Source Files
By default, class members are accessed after first creating an instance of the class and assigning it to a variable. You then access the member of the class by using the variable name followed by a dot and the member name. Static class members are an exception to this pattern. They're members that you access on the class directly, not on instances of the class. Here I've got a class named WebDeveloper that extends the Developer class. I'm first going to add two static members to this class, a property named jobDescription, and a method named logFavoriteProtocol. The syntax is identical to any other property or method declaration, with the exception of the static keyword that appears just before the member name. There will now only be a single instance of each of these members, and they will exist on the class itself. I'll add another method to the class that will log the value in the job description property. Normally, if you want to refer to another class member you use the this keyword in front of the member name; however, with static members you use the class name, since the member exists on the class. The same is true if you want to call static methods. Rather than creating a new instance of the class to call a method you just use the name of the class followed by a dot and the name of the static member. Static members are a nice way to add utility or helper methods that are related to the purpose of the class, but aren't dependent on any data that might be stored in instances of the class.
The last topic I want to cover related to classes is constructors. They're a special type of function that's executed when new instances of the class are created. They're a handy place for you to perform any special initialization or set up that needs to be performed for new instances of the class. Let's look at a couple of examples. Constructors appear inside the body of a class just like any other class member. The syntax is very similar to other methods, except the name is always constructor. This is about the simplest possible example of a constructor. It takes no parameters and just logs a message letting us know a new developer instance is being created. I should mention that you aren't required to add a constructor function to your classes. If you don't need to perform any special initialization, then you're free to leave it off. Let's look at a slightly more complex example. The constructor for this class has a few more interesting pieces. First of all, you can see that it accepts a string parameter named editor. This means that when new instances of the class are created with the new keyword a string must be passed to the constructor at that time. The first line inside the body of the constructor calls a special function named super. Based on the name, you may expect this function to execute some truly extraordinary code. That may or may not be the case. Calling super is how you call the parent classes constructor from a child class. This webDeveloper class extends the Developer class. The class being extended is sometimes referred to as the parent class or the super class. If your class extends some other class, and your class has a constructor, then you're required to call super, as I've done here. The last line of code in this constructor assigns to the favoriteEditor property the editor parameter that was passed in. Initializing properties like this is one of the most common ways to use constructors. Notice that I've put a modifier I haven't yet discussed in front of the favoriteEditor property. The readonly modifier does exactly what you would expect. It prevents the value of the property from being changed once it's set. Properties declared as readonly may only be initialized when they're declared or inside a constructor, as I've done here. Another useful feature of constructors that can save you some typing is to use parameter properties. I'll show you how to use them in the next demo.
Demo: Refactoring the Demo App with Classes
In this demo I'm going to refactor much of the code in the MultiMath app to use classes. Along the way I'll take advantage of a static member and show you how to use parameter properties. I'm going to start here in the app.ts file. I want to move the getInputValue function I wrote earlier into it's own utility class, so I'm first going to copy it to the clipboard. I'll add a new file to the project named utility.ts. I'll create a new class in it simply named Utility, and then paste in the function. Because the function is now part of a class I don't need the function keyword in front of the function name. I want to be able to use this function throughout the app without having to create new instances of the utility class, so I'll add the static keyword in front of it. I'm now going to refactor it just to simplify things a bit. I'll remove the undefined from the union return type, so that it now just returns a string. I'll just have it return the value of the HTMLInputElement it finds, and then get rid of the if else blocks below. Okay, I now want to add a scoreboard class I can use to display scores to the screen. I'll create a new file named scoreboard.ts. I know it will need to use the result interface I defined in result.ts, so I'll add a triple slash directive at the top of the file that references it. Next, I'll stub out the class, and then add a private member named results that will be an array of objects that implement the result interface. I need a way to add new results to the array, so I'll add a new method named addResult that takes a result as a parameter and pushes it onto the array I just defined. I'll then use a code snippet to paste in another method named updateScoreboard. Its job is to generate the HTML representing all of the results and putting them on the screen. I declare a variable to hold the HTML and then just loop over the results array adding information from each result to the HTML. I then get a reference to the element on the page named scores, and add the new HTML to it. That's all I need it to do right now. The last class I want to add is a class to represent each game the player plays. I'll create a new file named game.ts. I know this class is going to need references to several other files, so I'll paste them in at the top. I'll then stub out the Game class, and paste an initial set of properties and a constructor. What I've got here is actually a very common pattern that you'll see as you start creating more classes with constructors. It's helpful to have your constructors take parameters, which you then use to initialize properties on the class. That's exactly what I'm doing here. I've got a private member named Scoreboard I'm initializing to a new scoreboard instance, but after that I've got three properties named player, problemCount, and factor, which I'm just setting to the values passed into the constructor. This is so common that the smart folks working on TypeScript created a shortcut to save us some typing. The first constructor parameter named newPlayer receives the value that's then just assigned to the public player property. I'm going to remove the parameter name and replace it with the name of the property I really want to receive the value, and then put the public access modifier in front of it. This effectively declares the new class property and initializes it with the value passed to the constructor. I can now delete the line above that declares the property and the line inside the constructor that assigns it a value. The code is getting smaller already, and I haven't changed the functionality at all. I'll now do the exact same thing for the problemCount and factor properties. These properties are known as parameter properties, since they're really created as part of parameters being passed to the constructor. You just need to add an access modifier in front of the property name in the constructor's signature. All of mine here are public, but you can use private, protected, and readonly as well. It's now much tidier. I now need to paste in a couple of methods on this class. The first one is named displayGame, and will generate the HTML that displays the game board on the screen. It uses the problemCount and factor properties on the class to determine exactly what the board should contain. I'm not going to go over all of the HTML it generates, but I do want to point out my use of the built-in string constructor. The factor property is a number, and I need to convert it to a string to display on the screen. The StringConstructor can be passed any value, and it will attempt to convert it to a string. Once the HTML was generated I added to the page and then enabled the calculate button. The other function I'm going to add is named calculateScore. It's going to read the answers provided by the player and create a result object that can then be added to the scoreboard. It loops over all the problems and compares the provided answer to the correct answer in this if block. If they match it increments the value of the score variable. I then declare a new variable named result that will implement the result interface, and set it equal to object literal that has all the correct properties assigned values from this particular game. Once I have that result object I can use my scoreboard instance to add the result to the scoreboard and call the updateScoreboard function that will update the screen. The last thing I do here is disable the calculate button to get ready for the next game. Okay, that's all the pieces that I need to make the game work. I now just need to wire it all together in the app.ts file. I'll go back to it, and I'm going to delete all of the code in here and start fresh using my new classes. The first thing I know I need is references to the player and game classes, so I'll add those to the top. I'll then declare a variable to represent the new game. I'll use a code snippet to paste in the event handlers I need for the start game and calculate buttons. When the Start Game button is clicked I create a new player instance, and set its name to the value provided by the user. Notice that I'm using the static getInputValue function by calling it directly on the Utility class without first creating a new instance of the class. then retrieve the number of problems and the multiplication factor to use from the input boxes. I'm using the built-in number constructor to convert the input strings to numbers. The number constructor is very similar to the string constructor I showed you earlier. The last thing I need to do here is create a game instance and assign it to the new game variable by passing the player, problem, count, and factor values to the constructor. Calling the displayGame function will display the game board on the screen. The event handler for the calculate button just calls the calculateScore method on the newGame instance. That should be all I need. Let's jump over to the browser and see how it works. I'll refresh the page, and then add my name as the player. I'll use the default factor of five, and just do three problems in this game. I'll click the Start Game button, and the simple game board is displayed. I'll now answer each of the, and click the Calculate Score button. The scoreboard gets updated, and it reports that I got three out of three correct for factor five. Everything is working, and we finally have a fully functional version of the game.
I've covered a lot of material in this module. The headliner topics were obviously interfaces and classes. Getting type checking support for built-in types is great, but being able to get the same support for types you create is immensely helpful when working on large applications. I showed you lots of syntax examples and use cases for classes and interfaces, but along the way you also learned about related topics, such as how TypeScript's structural type system works, and how to configure a project to use multiple source files, and only compile those that are needed. Most of all, I hope you saw the continuation of a theme, which is flexibility. TypeScript is tremendously flexible. You can choose to create custom types or not. You can choose to compile a lot of individual files or a single file. You can also choose different techniques for delivering those files to the browser. I'll show you another option for that as the theme of flexibility continues in the next module, which covers TypeScript modules. Stay tuned for that.
Creating and Consuming Modules
Introduction and Overview
Hey everybody. Welcome back. In this course module I'm going to cover TypeScript modules. TypeScript adopted the very straightforward ES2015 module syntax, and I'll show you how to use it, along with some features specific to TypeScript that will help you create and consume modules in your applications. I'll start off with a brief explanation of why you might want to use modules in TypeScript. Since not all browsers natively support modules yet I'll also quickly go over some of the supporting technologies you may have to employ in order to use your TypeScript modules in a browser application. I'll then cover the syntax you use to import and export modules and wrap up with an explanation of how TypeScript resolves the location of the modules you import.
Why Use Modules?
In the last course module I finished building a fully functional version of the MultiMath demo app that didn't use modules at all. Therefore, it would be very reasonable to ask why they're even needed. Well, for very small apps it may not make much of a difference, but as your app gets larger modules provide a number of benefits. They allow you to encapsulate implementation details inside the module and only expose a carefully considered API for other modules to consume. This gives you the freedom to refactor code inside the module and not effect consumers of the module, as long as the API for it doesn't change. Modules are also easily reusable. If designed to implement a very specific piece of functionality they can be used throughout an app or dropped into other apps as needed. I think perhaps the most important benefit of modules is that they allow you to think about your apps in terms of larger building blocks. They create higher level abstractions you can use when discussing and planning your application. I found this can be tremendously helpful when working on large projects. Modules are great, but depending on your runtime environment they can require some supporting technologies. I'll talk more about those next.
Exporting and Importing
The API exposed by a module is defined by the items that are exported from the module. Modules can export just about any of the TypeScript constructs we've used in this course; functions, classes, interfaces or even simple variables. Let's look at a snippet from a file named person.ts. Let's suppose it has an interface named Person that I want to make available to other modules. I can export the interface by just adding the export keyword in front of the interface declaration. It will then be available to be imported by other modules. I've left off the body of the interface just to save some space on the slide. You can also export function and class declarations the same way. Notice that the Employee class also includes the default keyword after the export keyword. This specifies that this will be the default item exported from this module. It's available for import just like any of the other items here, but if the importing module didn't specify the name of a particular item to import from this module, then it would be this class that would be imported. If there are functions, classes or other things I want to use in this module, but not have them exposed to other modules, then I can just define them as I normally would, and leave off the export keyword. That effectively makes them private to this module. Export statements serve much the same purpose as exporting a declaration. It's just a different syntax. Let's suppose I have the same person.ts file from the last slide, and what to export the same items from it with an export statement. Rather than placing the export keyword in front of each declaration I want to export, I just use the keyword once followed by a comma separated list of the names of the items I want to export inside curly braces. This list of exports doesn't have a default export like those in the last slide, but I am able to take advantage of an additional trick using this syntax. Notice that I added the as keyword after specifying that the employee class should be exported. That's followed by the alias I want to use for the exported class. The effect of this is that I can use the name Employee for the class inside this module, but it will appear to other modules that import it as StaffMember instead. If there are lots of items in a module you want to export, then using an export statement like this can be nice because you can easily see in one place exactly what's being exported. Otherwise, I think it's easier to just add the export keyword to the declarations. Use whichever method you prefer. To use the functionality exported from a module you must import it in the consuming module, and there are several different ways you can import from a module. In this example, I'm importing two of the exported items in the person module from the previous slide. I do this by using the import keyword followed by a comma separated list of the items from the module I want to import inside curly braces. Note that you're not required to import everything the module exports. You can just import what you need. After the list you use the from keyword followed by the module containing the items you're importing. This is what's known as a relative reference. Because it starts with dot slash TypeScript will look for a file named person with a valid TypeScript file extension in the same directory as the current file. I'll talk more about relative and nonrelative references when I discuss module resolution a little later. Once the items are imported I can use them like any other class, function or interface. Here I'm just declaring a new variable to have the Person type. Let's look at a few variations on this basic import statement. This example looks very similar, but notice that there is only one item being imported, and it's not surrounded by curly braces. This is one way you could import the default export from a module. The default export from the person module will be assigned the name Worker in this module regardless of what it was named in the Person module. Using this technique means I don't have to know the name of the default item being exported. I can just use an import statement and specify the name I want to give it. The default export from the earlier slide was the employee class, and I can create new instances of it here, like any other class, only here it's known as Worker. I can also import items and alias them to new names as part of the import statement. Here I'm importing the StaffMember class and specifying that I'll refer to it as CoWorker. I do that by using the as keyword followed by the alias I want to give it. Just like the previous example, I can use it like any other class here with that alias. The last example I want to show you here is how to import an entire module. Rather than listing the specific items you want to import from the module you use an asterisk followed by the as keyword and the name you want to use to refer to the imported module. Here I'm importing all of the person module and giving it the alias, HR. I can then access items in that module using the alias followed by a dot and the name of the item. Now that you've seen the basic syntax for working with modules, let's return to the demo app and refactor it to use modules.
Demo: Converting the Demo App to Use Modules
Relative vs. Non-relative Imports
In order to understand how TypeScript resolves the location of the modules you import you first need to understand the difference in relative imports and non-relative imports. Relative imports direct the compiler to a specific location on the file system where the module can be found. All relative references begin with either a slash, dot slash or dot dot slash to direct the compiler to the location of the file. Here the reference to the hardware module is preceded by a slash that tells the compiler the file may be found on the root of the file system. You can leave off the file extension, and the compiler will automatically look for files with valid TypeScript file extensions. These examples also use relative references. The compiler will look for the person module in the same directory as the current file, and it will go up one directory, and then look for the recruiting module inside the HR directory. Non-relative references are nearly identical, but they don't include any reference to a directory structure before the module name. These two lines import the jquery and lodash modules, but notice that they don't include any reference to paths. I'll show you how TypeScript finds the files referred to with non-relative references in just a second. In general, you should use relative references when referring to your own modules and non-relative references when referring to third party modules.
Module Resolution Strategies
TypeScript resolves the location of modules by first seeing if they're relative or non-relative references, and then attempting to locate the module using the configured module resolution strategy. That strategy is configured using the moduleResolution compiler option. The two possible values you can pass it are Classic and Node. The TypeScript documentation mentions that Classic mode is available primarily for backward compatibility, but I think it's important to have a general understanding of the differences between the two. The first thing to know is that the default value for the moduleResolution compiler option is dependent on what type of modules you've configured the compiler to emit. Classic mode is the default resolution strategy if you're emitting AMD, System or ES2015 modules. For all other module types node is the default resolution strategy. The process of resolving modules in classic mode is very simple. I'll show you some examples in a minute, but it's little more than traversing directories looking for the right module. Setting the mode to Node, as you might expect, closely mirrors the way Node attempts to resolve modules. It's not terribly complex, but there is certainly more to it than what happens with classic mode. Classic mode is also less configurable. The node strategy will read a package.json file if it's present, so it's somewhat more configurable. Let's now look at some examples of how each of these modes would attempt to resolve both relative and non-relative references.
Module Resolution Examples
Demo: Configuring Module Resolution
Demo: Configuring a Module Loader
Being More Productive with Type Declaration Files
Introduction and Overview
What Are Type Declaration Files?
Installing Type Declaration Files
Demo: Installing and Using a Type Declaration File
Brice has been a professional developer for over 20 years and loves to experiment with new tools and technologies. Web development and native iOS apps currently occupy most of his time.
Released18 May 2017