Book Image

Mastering TypeScript 3 - Third Edition

By : Nathan Rozentals
Book Image

Mastering TypeScript 3 - Third Edition

By: Nathan Rozentals

Overview of this book

TypeScript is both a language and a set of tools to generate JavaScript. It was designed by Anders Hejlsberg at Microsoft to help developers write enterprise-scale JavaScript. Starting with an introduction to the TypeScript language, before moving on to basic concepts, each section builds on previous knowledge in an incremental and easy-to-understand way. Advanced and powerful language features are all covered, including asynchronous programming techniques, decorators, and generics. This book explores many modern JavaScript and TypeScript frameworks side by side in order for the reader to learn their respective strengths and weaknesses. It will also thoroughly explore unit and integration testing for each framework. Best-of-breed applications utilize well-known design patterns in order to be scalable, maintainable, and testable. This book explores some of these object-oriented techniques and patterns, and shows real-world implementations. By the end of the book, you will have built a comprehensive, end-to-end web application to show how TypeScript language features, design patterns, and industry best practices can be brought together in a real-world scenario.
Table of Contents (16 chapters)
Free Chapter
1
TypeScript Tools and Framework Options

What is TypeScript?

TypeScript is a programming language designed by Anders Hejlsberg, the founder of the C# language. It is the result of an assessment of the JavaScript language, and what could be done to help developers when writing JavaScript. TypeScript includes a compiler which will transform code written in TypeScript into JavaScript. It's beauty is really in its simplicity. We can take existing JavaScript, add a few TypeScript keywords here and there, and transform our code into a strongly-typed, object-oriented, syntax-checked code base. By adding a compile step, we can validate that we have written sound JavaScript that is going to behave as we intended it to.

TypeScript generates JavaScript—it's as simple as that. This means that wherever JavaScript can be used, TypeScript can be used to generate the same JavaScript, but with added compile-time validations to ensure that it does not break certain rules. Having these extra validations before we even run the JavaScript is an immense time-saver, particularly where development teams are large, or where the resulting JavaScript is published as a library.

TypeScript also includes a Language Service, which can be used by tools such as code editors to help us understand how we should use JavaScript functions and libraries. These editors can then automatically provide a programmer with code suggestions and hints on how to use these libraries.

The TypeScript language, it's compiler, and associated tools helps JavaScript developers to be more productive, find bugs quicker, and help each other understand how their code should be used. It allows us to use tried and tested object-oriented concepts and Design patterns in our JavaScript code in a very simple and easy to understand manner. Let's try to understand how it does this.

JavaScript and the ECMAScript Standard

JavaScript as a language has been around for a long time. Originally designed as a language to support HTML within a single web browser, it inspired multiple clones of the language, each with its own implementations. Eventually, a global standard was introduced, allowing websites to support multiple browsers. The language defined in this standard is called ECMAScript.

Each JavaScript interpreter must deliver functions and features that conform to the ECMAScript standard. The ECMAScript standard that was published in 1999 was officially called ECMA-262, 3rd edition, but became known simply as ECMAScript 3. This version of JavaScript became widely adopted and formed the basis for the explosive popularity and growth of the internet as we know it.

With the popularity of the language, and the increase in usage outside of a web browser, the ECMAScript standard has been revised and updated a number of times. Unfortunately, the time it takes between proposing new language features and the ratification of a new standard to cover them can be rather lengthy. Even when a new version of the standard is published, web browsers only adopt these standards over time, and may also implement parts of the standard before others.

Before choosing which standard to adopt, therefore, it is important to understand which browsers, or more accurately, which runtime engine will need to be supported. To support these decisions, there are a number of reference sites that list support in what is known as a compatibility table.

Currently, there are three main versions of ECMAScript to choose from: ES3, ES5 and the newly ratified ES6. ES3 has been around for a long time, and pretty much any web browser will support it. ES5 is supported by most modern web browsers. ES6 is the latest version of the standard, and by far the biggest update to the language thus far. It introduces classes into the language for the first time, making object-oriented programming easier to implement.

The TypeScript compiler has a parameter that can switch between different versions of the ECMAScript standard. TypeScript currently supports ES3, ES5, and ES6. When the compiler runs over your TypeScript, it will generate compile errors if the code you are attempting to compile is not valid for that standard. The team at Microsoft has committed to follow the ECMAScript standards in any new versions of the TypeScript compiler, so, as and when new editions are adopted, the TypeScript language and compiler will follow suit.

The benefits of TypeScript

To give you a flavor of the benefits of TypeScript, let's take a very quick look at some of the things that TypeScript brings to the table:

  • Compilation
  • Strong typing
  • Type definitions for popular JavaScript libraries
  • Encapsulation
  • Public and private accessors

Compiling

One of the most-loved features of JavaScript is the lack of a compilation step. Simply change your code, refresh your browser, and the interpreter will take care of the rest. There is no need to wait for a while until the compiler is finished in order to run your code.

While this may be seen as a benefit, there are many reasons why you would want to introduce a compilation step. A compiler can find silly mistakes, such as missing braces or missing commas. It can also find other more obscure errors, such as using a single quote (') where a double quote (") should have been used. Every JavaScript developer will tell horror stories of hours spent trying to find bugs in their code, only to find that they have missed a stray closing brace } , or a simple comma ,.

Introducing a compilation step into your workflow really starts to shine when managing a large code base. There is an old adage that states that we should fail early and fail loudly, and a compiler will shout very loudly at the earliest possible stage when errors are found. This means that any check-in of source code will be free from bugs that the compiler has identified.

When making changes to a large code base, we also need to ensure that we are not breaking any existing functionality. In a large team, this often means using the branching and merging features of a source code repository. Running a compilation step before, during, and after merges from one branch to another gives us further confidence that we have not made any mistakes, or that the automatic merge process has not made any mistakes either.

If a development team is using a continuous integration process, the continuous integration (CI) server can be responsible for building and deploying an entire site, and then running a suite of unit and integration tests on the newly checked-in code. We can save hours of build time and hours of testing time by ensuring that there are no syntax errors in the code, before we embark on deploying and running tests.

Lastly, as mentioned before, the TypeScript compiler can be configured to output ES3, ES5, or ES6 JavaScript. This means that we can target different runtime versions from the same code base.

Strong typing

JavaScript is not strongly typed. It is a language that is very dynamic, as it allows objects to change their properties and behavior on the fly. As an example of this, consider the following code:

var test = "this is a string"; 
console.log('test=' + test); 
 
test = 1; 
console.log('test=' + test); 
 
test = function (a, b) { 
    return a + b; 
} 
console.log('test=' + test); 

On the first line of this code snippet, a variable named test is declared, and assigned a string value. To ensure that this is the case, we have logged the value to the console. We then assign a numeric value to the test variable, and again log its value to the console. Note, however the final snippet of code. We are assigning a function that takes two parameters to the test variable. If we run this code, we will get the following results:

test = this is a string
test = 1
test = function (a, b) {
return a + b;
}

Here, we can clearly see the changes we are making to the test variable. It changes from a string value to a numeric value, and then becomes a function.

Changing the type of a variable at runtime can be a very dangerous thing to do, and can cause untold problems. This is why traditional object-oriented languages enforce strict typing. In other words, they do not allow the nature of a variable to change once declared.

While all of the preceding code is valid JavaScript – and could be justified – it is quite easy to see how this could cause runtime errors during execution. Imagine that you were responsible for writing a library function to add two numbers, and then another developer inadvertently reassigned your function to subtract these numbers instead.

These sorts of errors may be easy to spot in a few lines of code, but it becomes increasingly difficult to find and fix these as your code base and development team expands.

TypeScript's syntactic sugar

TypeScript introduces a very simple syntax to check the type of an object at compile time. This syntax has been referred to as syntactic sugar, or more formally, type annotations. Consider the following version of our original JavaScript code, but written in TypeScript:

var test: string = "this is a string"; 
test = 1; 
test = function(a, b) { return a + b; } 

Note that on the first line of this code snippet, we have introduced a colon : and a string keyword between our variable and its assignment. This type annotation syntax means that we are setting the type of our variable to be of type string, and that any code that does not adhere to these rules will generate a compile error. Running the preceding code through the TypeScript compiler will generate two errors:

hello.ts(3,1): error TS2322: Type 'number' is not assignable to type 'string'.
hello.ts(4,1): error TS2322: Type '(a: any, b: any) => any' is not assignable to type 'string'.

The first error is fairly obvious. We have specified that the variable test is a string, and therefore attempting to assign a number to it will generate a compile error. The second error is similar to the first, and is in essence saying that we cannot assign a function to a string.

In this way, the TypeScript compiler introduces strong, or static typing to our JavaScript code, giving us all of the benefits of a strongly typed language. TypeScript is therefore described as a superset of JavaScript. We will explore this in more detail in Chapter 2, Types, Variables, and Function Techniques.

Type definitions for popular JavaScript libraries

As we have seen, TypeScript has the ability to annotate JavaScript, and bring strong typing to the JavaScript development experience. But how do we strongly type existing JavaScript libraries? The answer is surprisingly simple: by creating a definition file. TypeScript uses files with a .d.ts extension as a sort of header file, similar to languages such as C++, in order to superimpose strong typing on existing JavaScript libraries. These definition files hold information that describes each available function and or variables, along with their associated type annotations.

Let's have a quick look at what a definition would look like. As an example, consider a function from the popular Jasmine unit testing framework called describe:

var describe = function(description, specDefinitions) { 
  return jasmine.getEnv().describe(description, specDefinitions); 
}; 

Note that this describe function has two parameters – description and specDefinitions. But JavaScript does not tell us what sort of variables these are. We would need to have a look at the Jasmine documentation to figure out how to call this function: If we head over to http://jasmine.GitHub.io/2.0/introduction.html , we will see an example of how to use this function:

describe("A suite", function () { 
    it("contains spec with an expectation", function () { 
        expect(true).toBe(true); 
    }); 
}); 

From the documentation, then, we can easily see that the first parameter is a string, and the second parameter is a function. But there is nothing in JavaScript that forces us to conform to this API. As mentioned before, we could easily call this function with two numbers, or inadvertently switch the parameters around, sending a function first, and a string second. We will obviously start getting runtime errors if we do this, but TypeScript, using a definition file, can generate compile-time errors before we even attempt to run this code.

Let's have a look at a piece of the jasmine.d.ts definition file:

declare function describe( 
    description: string,  
    specDefinitions: () => void 
): void; 

This is the TypeScript definition for the describe function. Firstly, declare function describe tells us that we can use a function called describe, but that the implementation of this function will be provided at runtime.

Clearly, the description parameter is strongly typed to be a string, and the specDefinitions parameter is strongly typed to be a function that returns void. TypeScript uses the double braces () syntax to declare functions, and the arrow syntax to show the return type of the function. Hence, () => void is a function that does not return anything. Finally, the describe function itself will return void.

Imagine that our code were to try and pass in a function as the first parameter, and a string as the second parameter (clearly breaking the definition of this function), as shown in the following example:

describe(() => { /* function body */}, "description"); 

In this instance, TypeScript will generate the following error:

hello.ts(11,11): error TS2345: Argument of type '() => void' is not assignable to parameter of type 'string'.  

This error is telling us that we are attempting to call the describe function with invalid parameters. We will look at definition files in more detail in later chapters, but this example clearly shows that TypeScript will generate errors if we attempt to use external JavaScript libraries incorrectly.

DefinitelyTyped

Soon after TypeScript was released, Boris Yankov started a GitHub repository to house definition files, called DefinitelyTyped (http://definitelytyped.org) . This repository has now become the first port of call for integrating external libraries into TypeScript, and it currently holds definitions for over 1,600 JavaScript Libraries. The growth of this site, and the rate at which type definitions have been generated for many JavaScript libraries, shows the popularity of TypeScript.

Encapsulation

One of the fundamental principles of object-oriented programming is encapsulation, the ability to define data, as well as a set of functions that can operate on that data, into a single component. Most programming languages have the concept of a class for this purpose—providing a way to define a template for data and related functions.

Let's first take a look at a simple TypeScript class definition:

class MyClass { 
    add(x, y) { 
        return x + y; 
    } 
} 
 
var classInstance = new MyClass(); 
var result = classInstance.add(1,2); 
console.log(`add(1,2) returns ${result}`); 

This code is pretty simple to read and understand. We have created a class, named MyClass, with a simple add function. To use this class, we simply create an instance of it, and call the add function with two arguments.

JavaScript, prior to ES6, does not have a class statement, but instead uses functions to reproduce the functionality of classes. Encapsulation through classes is accomplished by either using the prototype pattern, or by using the closure pattern. Understanding prototypes and the closure pattern, and using them correctly, is considered a fundamental skill when writing enterprise-scale JavaScript.

A closure is essentially a function that refers to independent variables. This means that variables defined within a closure function remember the environment in which they were created. This provides JavaScript with a way to define local variables, and provide encapsulation. Writing the MyClass definition in the preceding code, using a closure in JavaScript, would look something like this:

var MyClass = (function () { 
    // the self-invoking function is the 
    // environment that will be remembered 
    // by the closure 
    function MyClass() { 
        // MyClass is the inner function, 
        // the closure 
    } 
    MyClass.prototype.add = function (x, y) { 
        return x + y; 
    }; 
    return MyClass; 
})(); 
var classInstance = new MyClass(); 
var result = classInstance.add(1, 2); 
console.log("add(1,2) returns " + result);  

We start with a variable called MyClass, and assign it to a function that is executed immediately – note the })(); syntax near the bottom of the closure definition. This syntax is a common way to write JavaScript in order to avoid leaking variables into the global namespace. We then define a new function named MyClass, and return this new function to the outer calling function. We then use the prototype keyword to inject a new function into the MyClass definition. This function is named add and takes two parameters, returning their sum.

The last few lines of the previous code snippet show how to use this closure in JavaScript. Create an instance of the closure type, and then execute the add function. Running this code will log add(1,2) returns 3 to the console, as expected.

Looking at the JavaScript code versus the TypeScript code, we can easily see how simple the TypeScript looks compared to the equivalent JavaScript. Remember how we mentioned that JavaScript programmers can easily misplace a brace {, or a bracket ( ? Have a look at the last line in the closure definition: })(); Getting one of these brackets or braces wrong can take hours of debugging to find.

TypeScript classes generate closures

The JavaScript as shown previously is actually the output of the TypeScript class definition. So, TypeScript actually generates closures for you.

Adding the concept of classes to the JavaScript language has been talked about for years, and is currently a part of the ECMAScript 6th Edition. Microsoft has committed to follow the ECMAScript standard in the TypeScript compiler, as and when these standards are published.

Public and private accessors

A further object-oriented principle that is used in encapsulation is the concept of data hiding—that is, the ability to have public and private variables. Private variables are meant to be hidden to the user of a particular class—as these variables should only be used by the class itself. Inadvertently exposing these variables can easily cause runtime errors.

Unfortunately, JavaScript does not have a native way of declaring variables private. While this functionality can be emulated using closures, a lot of JavaScript programmers simply use the underscore character (_) to denote a private variable. At runtime though, if you know the name of a private variable, you can easily assign a value to it. Consider the following JavaScript code:

var MyClass = (function() { 
    function MyClass() { 
        this._count = 0; 
    } 
    MyClass.prototype.countUp = function() { 
        this._count ++; 
    } 
    MyClass.prototype.getCountUp = function() { 
        return this._count; 
    } 
    return MyClass; 
}()); 
 
var test = new MyClass(); 
test._count = 17; 
console.log("countUp : " + test.getCountUp()); 

The MyClass variable is actually a closure, with a constructor function, a countUp function, and a getCountUp function. The _count variable is supposed to be a private member variable that is used only within the scope of the closure. Using the underscore naming convention gives the user of this class some indication that the variable is private, but JavaScript will still allow you to manipulate the _count variable. Take a look at the second last line of the code snippet. We are explicitly setting the value of _count to 17, which is allowed by JavaScript, but not desired by the original creator of the class. The output of this code would be countUp : 17.

TypeScript, however, introduces the public and private keywords that can be used on class member variables. Trying to access a class member variable that has been marked as private will generate a compile time error. As an example of this, the previous JavaScript code can be written in TypeScript as follows:

class CountClass { 
    private _count: number; 
    constructor() { 
        this._count = 0; 
    } 
    countUp() { 
        this._count ++; 
    } 
    getCount() { 
        return this._count; 
    } 
} 
 
var countInstance = new CountClass() ; 
countInstance._count = 17;  

Here, on the second line of our code snippet, we have declared a private member variable named _count. Again, we have a constructor, a countUp function, and a getCount function. If we compile this file, the compiler will generate an error:

hello.ts(39,15): error TS2341: Property '_count' is private and only accessible within class 'CountClass'. 
  

This error is generated because we are trying to access the private variable, _count, in the last line of the code.

The TypeScript compiler is therefore helping us to adhere to public and private accessors by generating a compile error when we inadvertently break this rule.

Remember, though, that these accessors are a compile-time feature only, and will not affect the generated JavaScript. You will need to bear this in mind if you are writing JavaScript libraries that will be consumed by third parties. Note that, by default, the TypeScript compiler will still generate the JavaScript output file, even if there are compile errors. This option can be modified, however, to force the TypeScript compiler not to generate JavaScript if there are compilation errors.