The JavaScript programming language was created by Brendan Eich and was added to the Netscape browser in 1995. Since that time, JavaScript has enjoyed enormous success and is now used to build server and desktop apps as well. However, this popularity and ubiquity have turned out to be a problem as well as a benefit. As larger and larger apps have been created, developers have started to notice the limitations of the language.
Large application development has greater needs than the browser development JavaScript was first created for. At a high level, almost all large application development languages, such as Java, C++, C#, and so on, provide static typing and OOP capabilities. In this section, we'll go over the advantages of static typing over JavaScript's dynamic typing. We'll also learn about OOP and why JavaScript's method of doing OOP is too limited to use for large apps.
But first, we'll need to install a few packages and programs to allow our examples. To do this, follow these instructions:
- Let's install Node first. You can download Node from here: https://nodejs.org/. Node gives us
npm
, which is a JavaScript dependency manager that will allow us to install TypeScript. We'll dive deep into Node in Chapter 8, Learning Server-Side Development with Node.js and Express.
- Install VSCode. It is a free code editor and its high-quality and rich features have quickly made it the standard development application for writing JavaScript code on any platform. You can use any code editor you like, but I will use VSCode extensively in this book.
- Create a folder in your personal directory called
HandsOnTypeScript
. We'll save all our project code into this folder. Important Note
If you don't want to type the code yourself, you can download the full source code as mentioned in the Technical requirements section.
- Inside
HandsOnTypeScript
, create another folder called Chap1
.
- Open VSCode and go to File | Open, and then open the Chap1 folder you just created. Then, select View | Terminal and enable the terminal window within your VSCode window.
- Type the following command into the terminal. This command will initialize your project so that it can accept
npm
package dependencies. You'll need this because TypeScript is downloaded as an npm
package:npm init
You should see a screen like this:
Figure 1.1 – npm init screen
You can accept the defaults for all the prompts as we will only install TypeScript for now.
- Install TypeScript with the following command:
npm install typescript
After all the items have been installed, your VSCode screen should look like this:
Figure 1.2 – VSCode after setup is complete
We've finished installing and setting up our environment. Now, we can take a look at some examples that will help us better understand the benefits of TypeScript.
Dynamic versus static typing
Every programming language has and makes use of types. A type is simply a set of rules that describe an object and can be reused. JavaScript is a dynamically typed language. In JavaScript, new variables do not need to declare their type and even after they are set, they can be reset to a different type. This feature adds awesome flexibility to the language, but it is also the source of many bugs.
TypeScript uses a better alternative called static typing. Static typing forces the developer to indicate the type of a variable up front, when they create it. This removes ambiguity and eliminates many conversion errors between types. In the following steps, we'll take a look at some examples of the pitfalls of dynamic typing and how TypeScript's static typing can eliminate them:
- On the root of the
Chap1
folder, let's create a file called string-vs-number.ts
. The .ts
file extension is a TypeScript specific extension and allows the TypeScript compiler to recognize the file and transpile it into JavaScript. Next, enter the following code into the file and save it:let a = 5;
let b = '6';
console.log(a + b);
- Now, in the terminal, type the following:
tsc string-vs-number.ts
tsc
is the command to execute the TypeScript compiler, and the filename is telling the compiler to check and transpile the file into JavaScript.
- Once you run the
tsc
command, you should see a new file, string-vs-number.js
, in the same folder. Let's run this file:node string-vs-number.js
The node
command acts as a runtime environment for the JavaScript file to run. The reason why this works is that Node uses Google's Chrome browser engine, V8, to run JavaScript code. So, once you have run this script, you should see this:
56
Obviously, if we add two numbers together normally, we want a sum to happen, not a string concatenation. However, since the JavaScript runtime has no way of knowing this, it guesses the desired intent and converts the a
number variable into a string and appends it to variable b
. This situation may seem unlikely in real-world code but if left unchecked it could occur, because in web development, most inputs coming in from HTML come in as strings—even if the user types a number.
- Now, let's introduce TypeScript's static typing into this code and see what happens. First, let's delete the
.js
file, as the TypeScript compiler may consider there to be two copies of the a
and b
variables. Take a look at this code:let a: number = 5;
let b: number = '6';
console.log(a + b);
- If you run the
tsc
compiler on this code, you will get the error Type "'6'" is not assignable to the type 'number'
. This is exactly what we want. The compiler tells us that there is an error in our code and prevents the compilation from successfully compiling. Since we indicated that both variables are supposed to be numbers, the compiler checks for that and complains when it finds it not to be true. So, if we fix this code and set b
to be a number, let's see what happens:let a: number = 5;
let b: number = 6;
console.log(a + b);
- Now, if you run the compiler, it will complete successfully, and running the JavaScript will result in the value
11
:
Figure 1.3 – Valid numbers addition
Great, when we set b
incorrectly, TypeScript caught our error and prevented it from being used at runtime.
Let's look at another more complex example, as it's like what you might see in larger app code:
- Let's create a new
.ts
file called test-age.ts
and add the following code to it:function canDrive(usr) {
console.log("user is", usr.name);
if(usr.age >= 16) {
console.log("allow to drive");
} else {
console.log("do not allow to drive");
}
}
const tom = {
name: "tom"
}
canDrive (tom);
As you can see, the code has a function that checks the age of a user and determines, based on that age, whether they are allowed to drive. After the function definition, we see that a user is created, but with no age property. Let's pretend that the developer wanted to fill that in later based on user input. Now, below that user creation, the canDrive
function is called and it claims the user is not allowed to drive. If it turned out that user tom
was over 16 years old and this function triggered another action to be taken based on the user's age, obviously this could lead to a whole host of issues.
There are ways in JavaScript to deal with this problem, or at least partially. We could use a for
loop to iterate through all of the property key names of the user object and check for an age
name. Then, we could throw an exception or have some other error handler to deal with this issue. However, if we had to do this on every function, it would become inefficient and onerous very quickly. Additionally, we would be doing these checks while the code is running. Obviously, for these errors, we would prefer catching them before they make it out to users. TypeScript provides a simple solution to this issue and catches the error before the code even makes it into production. Take a look at the following updated code:
interface User {
name: string;
age: number;
}
function canDrive(usr: User) {
console.log("user is", usr.name);
if(usr.age >= 16) {
console.log("allow to drive");
} else {
console.log("do not allow to drive");
}
}
const tom = {
name: "tom"
}
canDrive (tom);
Let's go through this updated code. At the top, we see something called an interface and it is given a name of User
. An interface is one possible kind of type in TypeScript. I'll detail interfaces and other types in later chapters, but for now, let's just take a look at this example. The User
interface has the two fields that we need: name
and age
. Now, below that, we see that our canDrive
function's usr
parameter has a colon and the User
type on it. This is called a type annotation and it means that we are telling the compiler only to allow parameters of the User
type to be given to canDrive
. Therefore, when I try and compile this code with TypeScript, the compiler complains that when canDrive
is called, age
is missing from the passed-in parameter, because our tom
object does not have that property:
Figure 1.4 – canDrive error
- So, once again, the compiler has caught our error. Let's fix this issue by giving
tom
a type:const tom: User = {
name: "tom"
}
- If we give
tom
a type of User
, but do not add the required age
property, we get the following error: Property 'age' is missing in type '{ name: string; }' but required in type 'User'.ts(2741)
However, if we add the missing age
property, the error goes away and our canDrive
function works as it should. Here's the final working code:
interface User {
name: string;
age: number;
}
function canDrive(usr: User) {
console.log("user is", usr.name);
if(usr.age >= 16) {
console.log("allow to drive");
} else {
console.log("do not allow to drive");
}
}
// let's pretend sometime later someone else uses the //function canDrive
const tom: User = {
name: "tom",
age: 25
}
canDrive (tom);
This code provides the required age
property in the tom
variable so that when canDrive
is executed, the check for usr.age
is done correctly and the appropriate code is then run.
Here's a screenshot of the output once this fix is made and the code is run again:
Figure 1.5 – canDrive successful result
In this section, we learned about some of the pitfalls of dynamic typing and how static typing can help remove and protect against those issues. Static typing removes ambiguity from code, both to the compiler and other developers. This clarity can reduce errors and make for higher-quality code.
Object-oriented programming
JavaScript is known as an OOP language. It does have some of the capabilities of other OOP languages, such as inheritance. However, JavaScript's implementation is limited both in terms of available language features and design. In this section, we'll take a look at how JavaScript does OOP and how TypeScript improves upon JavaScript's capabilities.
First, let's define what OOP is. There are four major principles of OOP:
- Encapsulation
- Abstraction
- Inheritance
- Polymorphism
Let's review each one.
Encapsulation
A shorter way of saying encapsulation is information hiding. In every program, you will have data and functions that allow you to do something with that data. When we use encapsulation, we are taking that data and putting it into a container of sorts. This container is known as a class in most programming languages and basically, it protects that data so that nothing outside of the container can modify or view it. Instead, if you want to make use of the data, it must be done through functions that are controlled by the container object. This method of working with object data allows strict control of what happens to that data from a single place in code, instead of being dispersed through many locations across a large application—which can be unwieldy and difficult to maintain.
There are some interpretations of encapsulation that focus mainly on the grouping of members inside a common container. However, in the strict sense of encapsulation, information hiding, JavaScript does not have this capability built in. For most OOP languages, encapsulation requires the ability to explicitly hide a member via a language facility. For example, in TypeScript, you can use the private
keyword so that a property cannot be seen or modified outside of its class. Now, it is possible in JavaScript to simulate member privacy through various workarounds, but again this is not part of the native code and adds additional complexity. TypeScript supports encapsulation with access modifiers such as private
natively.
Important Note
Privacy for class fields will be supported in ECMAScript 2020. However, as this is a newer feature, it is not supported across all browsers at the time of writing.
Abstraction
Abstraction is related to encapsulation. When using abstraction, you hide the internal implementation of how data is managed and provide a more simplified interface to the outside code. Primarily, this is done to cause "loose coupling." This means that it is desirable for code that is responsible for one set of data to be independent and separated from other code. In this way, it is possible to change the code in one part of the application without adversely affecting the code in another part.
Abstraction for most OOP languages requires the use of a mechanism to provide simplified access to an object, without revealing that object's internal workings. For most languages, this is either an interface or an abstract class. We'll review interfaces more deeply in a later chapter, but for now, interfaces are like classes whose members have no actual working code. You can consider them a shell that only reveals the names and types of object members, but hides how they work. This capability is extremely important in producing the "loose coupling" mentioned previously and allowing code to be more easily modified and maintained. JavaScript does not support interfaces or abstract classes, while TypeScript supports both features.
Inheritance
Inheritance is about code reuse. For example, if you needed to create objects for several types of vehicles—car, truck, and boat—it would be inefficient to write distinct code for each vehicle type. It would be better to create a base type that has the core attributes of all vehicles, and then reuse that code in each specific vehicle type. This way, we write some of the needed code only once and share it across each vehicle type.
Both JavaScript and TypeScript support classes and inheritance. If you're not familiar with classes, a class is a kind of type that stores a related set of fields and also may have functions that can act on those fields. JavaScript supports inheritance by using a system called prototypical inheritance. Basically, what this means is that in JavaScript, every object instance of a specific type shares the same instance of a single core object. This core object is the prototype, and whatever fields or functions are created on the prototype, they are accessible across the various object instances. This is a good way of saving resources, such as memory, but it does not have the level of flexibility or sophistication of the inheritance model in TypeScript.
In TypeScript, classes can inherit from other classes but they can also inherit from interfaces and abstract classes. Since JavaScript does not have these features, in comparison, its prototypical inheritance is limited. Additionally, JavaScript has no ability to inherit from multiple classes directly, which is another method of doing code reuse called multiple inheritance. But TypeScript does allow multiple inheritance using mixins. We'll dive deep into all these features later, but basically, the point is that TypeScript has a more capable inheritance model that allows for more kinds of inheritance and therefore more ways to reuse code.
Polymorphism
Polymorphism is related to inheritance. In polymorphism, it is possible to create an object that can be set to one of any number of possible types that inherit from the same base lineage. This capability is useful for scenarios where the type needed is not immediately knowable but can be set at runtime once the appropriate circumstances have arisen.
This feature is used less often in OOP code than some of the other features, but nevertheless can be useful. In the case of JavaScript, there is no direct language support for polymorphism, but due to its dynamic typing, it can be simulated reasonably well (some JavaScript enthusiasts will strongly disagree with this statement, but please hear me out).
Let's look at an example. It is possible to use JavaScript class inheritance to create a base class and have multiple classes that inherit from this one parent base class. Then, by using standard JavaScript variable declaration, which does not indicate the type, we can set the type instance at runtime to whichever inheriting class is appropriate. The issue I find is that there is no way to force the variable to be of a specific base type since there is no way to declare types in JavaScript, therefore there is no way of enforcing only classes that inherit from the one base type during development. So, again, you have to resort to workarounds such as using the instanceof
keyword in order to test for certain types at runtime, to try and enforce type safety.
In the case of TypeScript, static typing is on by default and forces type declaration when the variable is first created. Additionally, TypeScript supports interfaces, which can be implemented by classes. Therefore, declaring a variable to be of a specific interface type forces all classes instantiated to that variable to be inheritors of the same interface. Again, this is all done at development time before code is deployed. This system is more explicit, enforceable, and reliable than the one in JavaScript.
In this section, we have learned about OOP and its importance in large application development. We've also understood why TypeScript's OOP capabilities are significantly better and more feature-rich than JavaScript's.