Overview
In this chapter, you will learn how to define classes and instantiate them to create objects. You will also learn how to define the data types that can be passed to a class using interfaces. By the end of this chapter, you will be able to build a basic class that includes data attributes, a constructor, methods, and an interface. You will be able to create classes that take in multiple objects as arguments to build dynamic behavior and confidently use TypeScript to generate HTML code.
Object-Oriented Programming (OOP) has been around since the 1960s and many popular programming languages utilize it, including Java, Ruby, and Python. Prior to OOP, developers typically followed the procedural programming style. Languages that utilize procedural programming processes run from the top of the code file to the bottom. Eventually, developers started wanting to wrap entire processes and data so that they could be called from different parts of a program at different times. And that's how OOP was born.
From a high-level perspective, OOP allows programs to wrap data and behavior together to create complete systems. So, instead of programs running code from top to bottom, as with procedural programs, OOP programs allow you to create code blueprints and establish rules for how a program will run, and then you can call those blueprints from other parts of an application.
Don't worry if that doesn't make sense quite yet – we're going to walk through exactly how to work with OOP in TypeScript in this chapter. And we're going to start by learning about the fundamental building blocks of OOP – classes and objects.
In the previous chapters, we've covered a wide assortment of topics, including various ways to declare variables, how to work with advanced types, aliases, union types, and assertions, and how to check for types. You've already added quite a bit of knowledge to your TypeScript skill set.
In this chapter, we're going to build a scoreboard application in TypeScript and will be learning about classes and objects along the way. Do not worry if you have no previous knowledge or familiarity with OOP, or how it applies to TypeScript. If you have some experience with classes and objects, then you can skip ahead to some of the more advanced material later in the chapter – though you may still benefit from a refresher on these key concepts.
Before we build out our class, let's take a step back and understand how classes work. You can think of a class as a blueprint. It establishes a structure for what we want to build and has some behavior inside it. Now, the class by itself does nothing. It is simply a blueprint. In order to work with it, we have to perform a process called instantiation.
Instantiation is the process of taking a class and creating an actual object of the class that we can use. Let's walk through an example to understand instantiation further. Imagine that you're building a house and, like a good builder, you have a blueprint of what you want to build. That blueprint is like our class. The blueprint for a home is simply a set of rules, attributes, and behavior for a home. A blueprint for a house defines elements such as square footage, the number of rooms, the number of bathrooms, and where the plumbing goes. Technically, a blueprint is simply a set of rules that are printed out or stored on a computer; it's not the house itself, or the program itself, in this case. In order to create the house, someone needs to take the blueprint and then actually build the house, and it's the same in programming.
A class by itself does nothing besides establishing the rules for the program. In order to work with the class, we need to create an instance or object of that class. So, returning to the building analogy, you can think of instantiation as taking the blueprint for the house and building it.
Let's look at the following code snippet to understand how classes and objects appear in TypeScript:
class Person {
name:string;
constructor(name) {
this.name = name;
}
read() {
console.log(this.name+ "likes to read.");
}
}
const obj = new Person("Mike");
obj.read();
Let's walk through each of the elements in the preceding code so that you can have a mental model of the key terminology associated with classes and objects in TypeScript, and then we'll go through an in-depth exercise where you will see how to work with each element:
In the next section, we will solve an exercise wherein we'll be building our first TypeScript class.
In this exercise, we'll build a class named Team and add a behavior or method named generateLineup inside it. We'll also create an object of this class and access its method. Perform the following steps to implement this exercise:
Note
The code files for this exercise can be found here: https://packt.link/UJXSY.
tsc scoreboard.ts
Once this command is executed, a scoreboard.js file is generated, as you can see in the following screenshot:
class Team {
}
Right now, this is simply an empty class that doesn't do anything. Let's fix that by adding some behavior to the class. We can add behavior by defining functions. For our Team class, we're going to generate a lineup, so we define a function called generateLineup, and it doesn't take in any arguments.
Note
From a syntax perspective, notice that we're using the class keyword. The term class is a reserved word in TypeScript and JavaScript, and it tells the compiler that we're about to define a class. In this case, we're calling the Team class.
class Team {
generateLineup() {
return "Lineup will go here…";
}
}
As you can see, functions in classes, which are also referred to as methods, look similar in syntax to standard functions in JavaScript. Now, our generateLineup method simply returns a string. Later in the chapter, we'll see how we can implement dynamic behavior in this method.
Once we've created a class and defined its behavior, we can create an object. In order to create an object of the Team class, we call the new keyword in front of the Team class name and assign that to a variable. In this case, we'll store the instantiated object in a variable called astros.
const astros = new Team();
Notice that in the preceding code, we're also adding parentheses after the Team class name, mimicking how we call functions in TypeScript.
With all of this in place, we can now use the astros variable to call the generateLineup method on it.
console.log(astros.generateLineup());
tsc scoreboard.ts
node scoreboard.js
Once we run the preceding commands, the following output is displayed in the terminal: Lineup will go here…
Hence, we've created our first class, and then from there, we've taken that class, that blueprint, and then used instantiation to create an object. From that point, we're able to call the method inside the class. Now that we've created a class and used its object to access its methods, in the next section, we'll explore the concept of the constructor.
In the previous section, we established the syntax for classes in TypeScript. Before we get started with the next phase of the previous program, let's take a step back and discuss an element that we're going to use, called the constructor. The concept of constructors can be confusing if you've never used them before.
Returning to our blueprint/house analogy, if a class is like a home's blueprint and an object is the home that is created, the constructor is the process of going to the hardware shop and purchasing the materials needed to build the home. A constructor is run automatically anytime that you create an object. Typically, constructors are used to do the following:
Note
More on constructors will be covered in Chapter 8, Dependency Injection in TypeScript.
The concept of this is one of the most confusing aspects of OOP. The this keyword refers to the instance of the class that is currently being executed. It has access to the data and behavior of the created object. Let's say we have the following code within a class:
constructor(name){
this.name = name;
}
In the preceding code, if this.name is referring to the instance of the class and the attribute of name, what does the name parameter in the constructor represent? In order to use data in our class, we need to have a mechanism for passing data into the object, and that's what the constructor parameters are doing. So, why do we need to assign this.name to name? It does seem redundant; however, it is helpful for understanding how variable scope works in TypeScript classes. We need to assign the values passed into the object to this.attributeName so that the other methods in the class can have access to the values. If we simply passed the value into the constructor and didn't perform the this.name assignment, the other methods in the class wouldn't have access to the name value. Now, let's extend the behavior of the program in the next exercise, where we will explore the attributes of the class.
In this exercise, we'll add attributes to the Team class, which we created in the previous exercise. We'll be using constructors to define and access the attributes of the objects. Perform the following steps to implement this exercise.
Note
In this exercise, we'll continue the work we performed earlier in the chapter with our Team class, so make sure to reference it as a starting point. The code files for this exercise can be found here: https://packt.link/Diuyl.
We begin by listing the names of the attributes at the top of the Team class and then we set the value with a constructor function by passing in a name parameter. From there, we set the value of this.name to the value that gets passed into the constructor function:
class Team {
name: string;
constructor(name) {
this.name = name;
}
generateLineup() {
return "Lineup will go here …";
}
}
When we create the astros object, the this keyword represents the object that was created.
const astros = new Team();
console.log(astros.generatLineup());
const bluJays = new Team();
console.log(blueJays.generateLineup());
In the preceding code, we've created another Team class object called blueJays. From there, we called the generateLineup method on the object. When we say this.name, what we're referring to is the instance of the class. This means that when we say this.name for the first object, we're referring to the astros object. And then, for the new object we've created, this.name is referencing the blueJays object.
Our generateLineup method has access to the value of name because we assigned it in the constructor.
const astros = new Team("Astros");
console.log(astros.generateLineup());
const blueJays = new Team("Blue Jays");
console.log(blueJays.generateLineup());
Note
If you ever get asked the difference between parameters and arguments in TypeScript, parameters are what you place inside the function's declarations in your class. Arguments are what you pass to an object or a function.
In order to pass arguments to a class, you can pass them in the same way that you do with functions, as you can see above. Additionally, when we perform an assignment such as this.name = name, this means that when an object is created, it can call the data value as well.
const astros = new Team("Astros");
//console.log(astros.generateLineup());
console.log(astros.name);
const blueJays = new Team("Blue Jays");
//console.log(blueJays.generateLineup());
console.log(blueJays.name);
tsc scoreboard.ts
node scoreboard.js
Once we run the preceding commands, the following output is displayed in the terminal:
Astros
Blue Jays
As you can see in the code in the previous step, when we call astros.name, this outputs the name value that was passed into the instantiated object. When we pass the name value Blue Jays into the new object, the new value is printed in the terminal.
We are now able to understand the basic workings of classes and objects. We've also learned how to pass data into an object via a constructor. Now it's time to extend that knowledge and see how we can integrate types directly into our classes.
Even though the current implementation works, we're not taking advantage of the key benefits that TypeScript offers. In fact, the current implementation is very close to how you would build a class in vanilla JavaScript. By using types in classes, we can define exactly how to work with the code, which will help to make our code more manageable and scalable.
A real-world example of this would be a React application that utilizes TypeScript versus vanilla JavaScript. One of the most common errors that developers run into is passing the wrong type of data to a class or method, resulting in an error for the user. Imagine accidentally passing a string to a class that requires an array. When the user tries to access the page that is associated with that class, they won't see any data, as the wrong data was passed to the method.
When you utilize TypeScript and types in a React class, the text editor won't allow the program to even compile as it will explain to you exactly what type of data is required by each class and process. In the next section, we'll solve an exercise wherein we'll integrate different types into our class.
In this exercise, we'll add another attribute named players inside our Team class. This parameter takes arrays of strings. Perform the following steps to implement this exercise:
Note
We'll continue the work we performed in the previous exercise with our Team class, so make sure to reference it as a starting point. The code files for this exercise can be found here: https://packt.link/tbav7.
players: string[];
constructor(name, players){
this.name = name;
this.players = players;
}
generateLineup(){
return this.players.join(", ");
}
const astrosPlayers = ["Altuve", "Bregman", "Correa", "Springer"];
const astros = new Team("Astros", astrosPlayers);
console.log(astros.generateLineup());
console.log(astros.name);
const blueJaysPlayers = ["Vlad", "Smoak", "Tellez", "Sogard"];
const blueJays = new Team("Blue Jays", blueJaysPlayers);
console.log(blueJays.generateLineup());
console.log(blueJays.name);
tsc scoreboard.ts
node scoreboard.js
Once we run the preceding commands, the following output is displayed in the terminal:
Altuve, Bregman, Correa, Springer
Astros
Vlad, Smoak, Tellez, Sogard
Blue Jays
We've now integrated types into our Team class. If you're able to view the names that you passed to the class in the console, this means that you're working with the class and their types properly. In the next section, we'll learn why interfaces are needed and how they are useful.
We'll go through a deep dive into TypeScript interfaces in the next chapter. But for now, just know that an interface allows you to describe the data passed to a class when you're creating an object. In the previous exercise code, if we hover over the Team class on Visual Studio Code, we get the following message:
As you can see in the preceding screenshot, the Visual Studio Code editor's IntelliSense is saying that the players parameter uses the any data type. It's not giving us any usage hints here, and this starts to speak to the reason why we need interfaces, because right now, the players array could be anything. It could be a string, it could be an object, and so on. This is essentially breaking one of the main benefits of using TypeScript in the first place. Ideally, our programs should be declarative to the point that we know exactly what type of data should be passed to our functions and classes. We're going to leverage interfaces in order to do that. The way you define an interface is by starting with the interface keyword followed by the name of the interface. The common convention in the TypeScript community is to start with a capital I, followed by whatever class you're building the interface for.
Once we have created the interface and update the constructor, we'll establish a way of defining our arguments and our types. This will break any of the previously created objects with the old argument syntax since the previous arguments no longer match up with our new interface. In the next section, we'll complete an exercise wherein we'll build an interface.
In this exercise, we'll build an interface and set the types of data that need to be passed to our functions and classes. Perform the following steps to implement this exercise:
Note
We'll continue the work we performed in the previous exercise with our Team class, so make sure to reference it as a starting point. The code files for this exercise can be found here: https://packt.link/FWUA6.
interface ITeam{
name: string;
players: string[];
}
constructor(args: ITeam){
this.name = args.name;
this.players = args.players;
}
Notice in the preceding code that, instead of listing out each of the parameters separately, we're declaring the exact structure that is needed for a Team object to be created. From that point, we're calling the name and players values from the args parameter since our parameter list has now been refactored to use a single argument.
const astros = new Team();
Now notice what happens when we hover over the parentheses. It says that it expected one argument but got zero. Look at the following screenshot to view the message:
const astros = new Team({
name
})
After adding in the name argument, we'll see the following error:
If you hover over the name attribute, you can see that TypeScript is helping us understand the other arguments we need to pass in, because the players property is missing. So, this is already giving us so much more information on how our class needs to work.
const astrosPlayers = ["Altuve", "Bregman", "Correa", "Springer"];
const astros = new Team({
name: "Astros",
players: astrosPlayers
});
console.log(astros.generateLineup());
console.log(astros.name);
const blueJaysPlayers = ["Vlad", "Smoak", "Tellez", "Sogard"];
const blueJays = new Team({
name: "Blue Jays",
players: blueJaysPlayers
});
console.log(blueJays.generateLineup());
console.log(blueJays.name);
tsc scoreboard.ts
node scoreboard.js
Once we run the preceding commands, the following output is displayed in the terminal:
Altuve, Bregman, Correa, Springer
Astros
Vlad, Smoak, Tellez, Sogard
Blue Jays
We've now built an interface and set the types of data that need to be passed to our functions and classes. Although we got the same output as we got in the previous exercise, we are now aware of what type of data needs to be passed to our functions and classes.
Another great benefit of using interfaces and object-based arguments with classes is that the arguments do not have to be in a specific order. You can pass in the keys in any order that you want, and the class can still parse them properly. If you use standard parameter names, you'll always need to know the order to pass arguments to the class and function.
Now that we have learned how to build an interface and have the ability to pass data, along with having some help from IntelliSense in knowing the types of data that we're passing in, we can actually generate some HTML. It's fun to see the code we write generate its own code. Part of the reason why we chose to include this example is that this is very close to the same type of process that you will be using when building React JS or Angular applications. At their very core, the goal of a standard React app is to leverage JavaScript/TypeScript code to render HTML code that can be rendered to the user.
In the next section, we'll complete an exercise wherein we generate HTML code and view it in the browser.
In this exercise, we will generate some HTML by cleaning up some of the code. We'll get rid of the name attribute and the interface. Perform the following steps to implement this exercise:
Note
We'll continue the work we performed in the previous exercise with our Team class, so make sure to reference it as a starting point. The code files for this exercise can be found here: https://packt.link/Bz5LV.
players: string[];
constructor(players){
this.players = players;
}
generateLineup(): string{
const playersWithOrderNumber =
this.players.map((player, idx) => {
return `<div>${idx + 1} - ${player}</div>`;
});
return playersWithOrderNumber.join("");
}
The map function is a helpful iterator tool that loops over the player array. You can pass it as a function that performs some type of operation. In the preceding code, the line `<div>${idx + 1} – ${player}</div>` states that in every iteration, each player's data is wrapped inside the HTML code. Also, each element that is returned is stored in a new array, playersWithOrderNumber.
Note
Notice the return type that we've declared for the generateLineup method. This means that we're telling the TypeScript compiler that the method will always return a string value. The reason why this is so important is that if any other part of the application calls this method and tries to perform a task that does not work with the string data type, they'll get a clear error and recommendation on how to fix it.
tsc scoreboard.ts
node scoreboard.js
Once we have run the preceding commands, the following output is displayed in the terminal:
In the preceding output, you'll see that we're getting HTML returned that prints out the lineup of players for both teams.
But let's not stop here. Let's see what this looks like in the browser.
Note
You may get a different image depending on your default browser; however, the text displayed will be the same as listed in the preceding screenshot.
You can see that we have a full lineup of players for both teams. However, we have not yet formatted the text on the page, and so it is difficult to ascertain the teams to which the players belong unless you have access to the code. We will be enhancing this page with more information and formatting as we progress in this chapter.
Note that we can pass the objects themselves to another class that will put them together for us and generate a full scoreboard. In the next section, we'll learn how to work with multiple classes and objects.
In this section, we're going to learn how to create a class that combines other classes to give us more advanced behavior. The reason why this is an important concept to understand is that you will need to implement this type of behavior in many different types of applications. For example, if you are building a contact form in a React application, you might need to have classes for an API, form elements, form validations, and other form features all working together. In the next section, we will look at an exercise where we'll combine classes.
In this exercise, we will be creating a scoreboard class that will allow us to pass in objects and work with their data and behavior. This will allow us to take instantiated objects that were created from other classes such as our Team class. Then, we're going to add in some other behavior that will generate a full scoreboard that shows off both the lineups along with the data. Perform the following steps to implement this exercise:
Note
We'll continue the work we performed in the previous exercise with our Team class, so make sure to reference it as a starting point. The code files for this exercise can be found here: https://packt.link/UY5NP.
class Scoreboard{
homeTeam: Team;
awayTeam: Team;
date: string;
}
In the preceding code, notice how we were able to call the Team class. This is because when we create a class, we're able to treat that class like a type in TypeScript. So, TypeScript now knows that our homeTeam and awayTeam data attributes must be a Team object. The date attribute will represent the date of the scoreboard. If we tried to pass in string, array, or anything else for a Team object, the program would not compile.
interface IScoreboard{
homeTeam: Team;
awayTeam: Team;
date: string;
}
This is similar to what we implemented with the ITeam interface, but with a nice twist. Because our homeTeam and awayTeam attributes are not associated with a basic data type such as string or number, we're letting the interface know that these values are required to be objects of the Team class.
tsc scoreboard.ts
When the preceding command is executed, the scoreboard.js file is created.
In the preceding screenshot, what we're essentially doing here is almost like a mini declaration file for this class. We're defining the shape of the class. If you remember, those interfaces and those declaration files do not get compiled down into JavaScript. You can confirm this by looking at the generated JavaScript code in the preceding screenshot.
Now that we've defined the interface, we have essentially defined the shape of our Scoreboard class.
constructor(args: IScoreboard){
this.homeTeam = args.homeTeam;
this.awayTeam = args.awayTeam;
this.date = args.date;
}
With this in place, any functions inside our Scoreboard class can work with these values.
scoreboardHtml(): string{
return `
<h1>${this.date}</h1>
<h2>${this.homeTeam.name}</h2>
<div>${this.homeTeam.generateLineup()}</div>
<h2>${this.awayTeam.name}</h2>
<div>${this.awayTeam.generateLineup()}</div>
`;
}
In the preceding code, we have an <h1> heading tag for date and an <h2> heading tag wrapping the team names. This is great, as even though the Scoreboard class has no knowledge of the Team class, the IDE can let us know that we have access to the name value. Lastly, we're able to call the Team functions. So, inside the <div> tags wrapper, we're calling the generateLineup() function of Team, which we know from earlier returns a list of HTML elements. Also, notice that this function will always return a string and that we're using backticks so that we can use string literals, which can be dynamic.
Note
In TypeScript and JavaScript, string literals can be written on multiple lines, which is not allowed with quotation marks.
name: string;
players: string[];
constructor(name, players){
this.name = name;
this.players = players;
}
const astrosPlayers = ["Altuve", "Bregman", "Correa", "Springer"];
const astros = new Team("Astros", astrosPlayers);
//console.log(astros.generateLineup());
const blueJaysPlayers = ["Vlad", "Smoak", "Tellez", "Sogard"];
const blueJays = new Team("Blue Jays", blueJaysPlayers);
//console.log(blueJays.generateLineup());
const todaysGame = new Scoreboard({
date: "5/24/19",
homeTeam: astros,
awayTeam: blueJays
});
console.log(todaysGame.scoreboardHtml());
tsc scoreboard.ts
node scoreboard.js
Once we run the preceding commands, the following output is displayed in the terminal:
Finally, we combined two classes, namely, Scoreboard and Team. In the Scoreboard class, we created attributes of the Team type and added a few behaviors that will help to generate a full scoreboard consisting of the lineups of both teams.
So far, we've introduced classes and objects in TypeScript, and with this knowledge, we're ready to move on to the code activity in the next section, where we will create a user model.
In this activity, you will build a user authentication system that mimics how a TypeScript application would pass login data to a backend API to register and sign users into our baseball scorecard application. This will entail building multiple TypeScript classes and combining classes and objects together to mimic an authentication feature. Perform the following steps to implement this activity:
The expected output should look something like this:
Validating user...User is authenticated: true
Validating user...User is authenticated: false
Note
The solution to this activity can be found via this link.
Learning OOP development patterns for the first time can be a challenging task. In this chapter, you learned about OOP development, how to define classes in TypeScript, how to instantiate classes and create objects, how to combine data and methods in a class to encapsulate a full set of behavior, how to utilize interfaces in order to define the data that can be passed to a TypeScript class, and finally, how to pass the objects to classes of various types.
You also now have a basic understanding of how an authentication system works and how to utilize TypeScript to generate HTML code.
Now that you have a basic understanding of how classes and objects work in TypeScript, in the next chapter, you'll learn how to work with the concept of class inheritance and take a deeper dive into interfaces.
18.222.119.148