1. TypeScript Fundamentals

Overview

In this chapter, we'll briefly illustrate the problems that exist in JavaScript development environments, and we'll see exactly how TypeScript helps us write better and more maintainable code. This chapter will first help you set up the TypeScript compiler and then teach you the fundamentals. Additionally, we'll begin our journey into types, as they are the core feature of TypeScript – it's right in the name. Finally, you will be able to test your newly gained TypeScript skills by creating your own library.

Introduction

The world of online applications has grown tremendously in the past few decades. With it, web-based applications have grown not only in size but also in complexity. JavaScript, a language that was originally thought of and used as a go-between between the core application logic and the user interface, is being seen in a different light. It is the de facto language with which web apps are being developed. However, it just was not designed for the building of large applications with lots of moving parts. Along came TypeScript.

TypeScript is a superset of JavaScript that provides lots of enterprise-level features that JavaScript lacks, such as modules, types, interfaces, generics, managed asynchrony, and so on. They make our code easier to write, debug, and manage. In this chapter, you will first learn how the TypeScript compiler works, how transpilation occurs, and how you can set up the compiler options to suit your needs. Then, you will dive straight into TypeScript types, functions, and objects. You will also learn how you can make your own types in TypeScript. Finally, you can test your skills by attempting to create your own library to work with strings. This chapter serves as a launchpad with which you can jump-start your TypeScript journey.

The Evolution of TypeScript

TypeScript was designed by Microsoft as a special-purpose language with a single goal – to enable people to write better JavaScript. But why was that an issue at all? To understand the problem, we have to go back to the roots of the scripting languages for the web.

In the beginning, JavaScript was designed to enable only a basic level of interactivity on the web.

Note

JavaScript was initially developed in 1995 by Brendan Eich for use in Netscape Navigator.

It was specifically not designed to be the main language that runs within a web page, but to be a kind of glue between the browser and the plugins, such as Java applets that run on the site. The heavy lifting was supposed to be done by the plugin code, with JavaScript providing a simple layer of interoperability. JavaScript did not even have any methods that would enable it to access the server. Another design goal for JavaScript was that it had to be easy to use for non-professional developers. That meant that the language had to be extremely forgiving of errors, and quite lax with its syntax.

For a few years, that was the task that JavaScript (or, more properly, ECMAScript, as it was standardized) was actually doing. But more and more web pages came into existence, and more and more of them needed dynamic content. Suddenly, people needed to use a lot of JavaScript. Web pages started getting more and more complex, and they were now being referred to as web applications. JavaScript got the ability (via AJAX) to access servers and even other sites, and a whole ecosystem of libraries appeared that helped us write better web applications.

However, the language itself was still lacking lots of features that are present in most languages – primarily features that are targeted toward professional developers.

Note

Some of the most talked-about features included a lack of module/namespace support, type-checked expressions, better scoping mechanisms, and better support for asynchronous functionality.

Since it was designed for small-scale usage, it was very troublesome to build, and especially to maintain, large applications built with JavaScript. On the other hand, once it was standardized, JavaScript became the only way to actually run code inside the browser. So, one solution that was popular in the 2000s was to make an emulation layer – a kind of a tool that enabled developers to use their favorite language to develop an application that will take the original source code as input and output equivalent JavaScript code. Such tools became known as transpilers – a portmanteau of the words "translator" and "compiler." While traditional compilers take source code as input and output machine code that can execute directly on the target machine, transpilers basically translated the source code from one language to another, specifically to JavaScript. The resulting code is then executed on the browser.

Note

The code actually gets compiled inside the browser, but that's another story.

There were two significant groups of transpilers present – ones that transpiled from an existing language (C#, Java, Ruby, and so on) and ones that transpiled from a language specifically designed to make web development easier (CoffeeScript, Dart, Elm, and so on).

Note

You can see a comprehensive list at https://packt.link/YRoA0.

The major problem with most transpilers was that they were not native to the web and JavaScript. The JavaScript that was generated was confusing and non-idiomatic – it looked like it was written by a machine and not a human. That would have been fine, except that generated mess was the code that was actually executing. So, using a transpiler meant that we had to forgo the debugging experience, as we could not understand what was actually being run. Additionally, the file size of the generated code was usually large, and more often than not, it included a huge base library that needed to load before we would be able to run our transpiled code.

Basically, by 2012 there were two options in sight – write a large web application using plain JavaScript, with all the drawbacks that it had, or write large web applications using a transpiler, writing better and more maintainable code, but being removed from the platform where our code actually runs.

Then, TypeScript was introduced.

Note

A video of the introductory lecture is available at https://channel9.msdn.com/Events/Build/2012/3-012.

Design Goals of TypeScript

The core idea behind it was one that, in hindsight, seems quite obvious. Instead of replacing JavaScript with another language, why not just add the things that are missing? And why not add them in such a way that they can be very reasonably removed at the transpiling step, so that the generated code will not only look and be idiomatic but also be quite small and performant? What if we can add things such as static typing, but in an optional way, so that it can be used as much or as little as we want? What if all of that existed while we're developing and we can have nice tooling and use a nice environment, yet we're still able to debug and understand the generated code?

The design goals of TypeScript, as initially stated, were as follows:

  • Extend JavaScript to facilitate writing large applications.
  • Create a strict superset of JavaScript (that is, any valid JavaScript is valid TypeScript).
  • Enhance the development tooling support.
  • Generate JavaScript that runs on any JavaScript execution environment.
  • Easy transfer between TypeScript and JavaScript code.
  • Generate clean, idiomatic JavaScript.
  • Align with future JavaScript standards.

Sounds like a pie-in-the-sky promise, and the initial response was a bit lukewarm. But, as time progressed, and as people actually tried it and started using it in real applications, the benefits became obvious.

Note

The author's lecture on TypeScript, which was the first one to be broadcast worldwide by a non-Microsoft employee, can be found at https://www.slideshare.net/sweko/typescript-javascript-done-right.

Two areas where TypeScript became a power player were JavaScript libraries and server-side JavaScript, where the added strictness of type checking and formal modules enabled higher-quality code. Currently, all of the most popular web development frameworks are either natively written in TypeScript (such as Angular, Vue, and Deno) or have tight integrations with TypeScript (such as React and Node).

Getting Started with TypeScript

Consider the following TypeScript program – a simple function that adds two numbers:

Example 01.ts

1 function add (x, y) {

2 return x + y;

3 }

No, that's not a joke – that's real-life TypeScript. We just did not use any TypeScript-specific features. We can save this file as add.ts and can compile it to JavaScript using the following command:

tsc add.ts

This will generate our output file, add.js. If we open it and look inside, we can see that the generated JavaScript is as follows:

Example 01.js

1 function add(x, y) {

2 return x + y;

3 }

Yes, aside from some spacing, the code is identical, and we have our first successful transpilation.

The TypeScript Compiler

We will add to the example, of course, but let's take a moment to analyze what happened. First of all, we gave our file the .ts file extension. All TypeScript files have this extension, and they contain the TypeScript source code of our application. But, even if our code is valid JavaScript (as in this case), we cannot just load the .ts files inside a browser and run them. We need to compile/transpile them using the tool called the "TypeScript compiler," or tsc for short. What this tool does is takes TypeScript files as arguments and generates JavaScript files as outputs. In our case, our input was add.ts and our output was add.js. The tsc compiler is an extremely powerful tool, and it has a lot of options that we're able to set. We can get a full list of the options using this command:

tsc --all

The most common and important ones are as follows:

  • –outFile: With this option, we can specify the name of the output file we want to be generated. If it's not specified, it defaults to the same name as the input file, but with the .js extension.
  • –outDir: With this option, we can specify the location of the output file(s). By default, the generated files will be in the same location as the source files.
  • –types: With this option, we can specify additional types that will be allowed in our source code.
  • –lib: With this option, we specify which library files need to be loaded. As there are different execution environments for JavaScript, with different default libraries (for example, browser JavaScript has a window object, and Node.js has a process object), we can specify which one we want to target. We can also use this option to allow or disallow specific JavaScript functionality. For example, the array.include method was added in the es2016 JavaScript version. If we want to assume that the method will be available, then we need to add the es2016.array.include library.
  • –target: With this option, we specify which version of the ECMAScript (that is, JavaScript) language we're targeting. That is, if we need to support older browsers, we can use the ES3 or ES5 values, which will compile our code to JavaScript code that will execute in any environment that supports, correspondingly, versions 3 and 5 of the JavaScript language. If, on the other hand, we know that we'll run in an ultra-modern environment, as the latest Node.js runtime, we can use the ES2020 target, or even ESNEXT, which is always the next available version of the ECMAScript language.
  • There are several more options; however, we have only discussed a few here.

Setting Up a TypeScript Project

Since the TypeScript compiler has lots of options, and we'll need to use quite a few of them, specifying all of them each and every time we transpile a file will get tedious very fast. In order to avoid that, we can save our default options in a special file that will be accessed by the tsc command. The best way to generate this special file called tsconfig.json is to use tsc itself with the --init option. So, navigate to the folder where you want to store your TypeScript project and execute the following command:

tsc --init

This will generate a tsconfig.json file with the most commonly used option. The rest of the options are commented out, so if we want to use some other set of options, we can simply uncomment what we need. If we ignore the comments (which include a link to the documentation about the options), we get the following content:

{

  "compilerOptions": {

    "target": "es5",

    "module": "commonjs",

    "strict": true,

    "esModuleInterop": true,

    "skipLibCheck": true,

    "forceConsistentCasingInFileNames": true

  }

}

You can see that each and every option in the tsconfig.json file has a corresponding command-line switch, for example, module, target, and so on. If a command-line switch is specified, it takes precedence. However, if a command-line switch is not defined, then tsc looks for the nearest tsconfig.json file up the directory hierarchy and takes the value specified there.

Exercise 1.01: Using tsconfig.json and Getting Started with TypeScript

In this exercise, we'll see how to command TypeScript using the tsconfig.json file. We'll see how to create TypeScript files and transpile them to JavaScript, based on the options we specify:

Note

Please make sure you have installed Visual Studio (VS) Code and followed the installation steps as mentioned in the Preface. The code files for this exercise can be found here: https://packt.link/30NuU.

  1. Create a new folder and execute the following command in a new terminal within it:

    tsc --init

  2. Verify that a new tsconfig.json file is created within the folder and that its target value is es5.
  3. Create a new file called squares.ts inside it.
  4. In squares.ts, create a function called squares:

    function squares(array: number[]) {

  5. Create a new array from the input argument, using the JavaScript map function with an arrow function argument:

        const result = array.map(x => x * x);

  6. Return the new array from the function:

        return result;

    }

  7. Save the file and run the following command in the folder:

    tsc squares.ts

  8. Verify that there is a new file in the folder called squares.js with the following content:

    function squares(array) {

        var result = array.map(function (x) { return x * x; });

        return result;

    }

    Here, we can see that the transpilation step did several things:

    - It removed the type annotation from the array: number[] parameter, transpiling it to array.

    - It changed the const result variable declaration to a var result declaration.

    - It changed the arrow function, x=>x*x, to a regular function, function (x) { return x * x; }.

    While the first is TypeScript-specific code, the second and third are examples of TypeScript's backward compatibility – both the arrow functions and the const declarations are JavaScript features that were introduced in the ES6 version of the language.

  9. Run the following command in the folder:

    tsc --target es6 squares.ts

    This will override the setting from the tsconfig.json file and it will transpile the TypeScript code to ES6-compatible JavaScript.

  10. Verify that the contents of the squares.js file are now as follows:

    function squares(array) {

        const result = array.map(x => x * x);

        return result;

    }

    You can note that, in contrast to the results in step 8, now the const keyword and the arrow functions are intact, because the target we specified supports them natively. This is an extremely important feature of TypeScript. With this feature, even if we don't use the rich type system that TypeScript provides, we can still write code in the most modern version of JavaScript available, and TypeScript will seamlessly transpile our code to a version that can actually be consumed by our customers.

Types and Their Uses

We've mentioned that TypeScript's type system is its distinguishing feature, so let's take a better look at it. JavaScript is what's called a loosely typed language. That means that it does not enforce any rules on the defined variables and their values. Consider, for example, that we define a variable called count and set it to the value of 3:

let count = 3;

There is nothing that prevents us from setting that variable to a value that is a string, a date, an array, or basically any object. All of the following assignments are valid:

count = "string";

count = new Date();

count = false;

count = [1, 2, 3];

count = { key: "value" };

In almost all scenarios, this is not a behavior we actually want. Moreover, since JavaScript does not know when we are writing the code whether a variable contains a string or a number, it cannot stop us from trying to, for example, convert it to lowercase. We cannot know whether that operation will succeed or fail until the moment we actually try it, when running the code.

Let's take the following example:

let variable;

if (Math.random()>0.5) {

    variable = 3;

} else {

    variable = "String";

}

console.log(variable.toLowerCase());

This code will either output "String" or throw a variable.toLowerCase is not a function error. The only way to determine whether this code will break is to actually run it. In a nutshell, in a loosely typed language, while values themselves have types, variables, on the other hand, don't. They just take the type of the value they are currently holding. So, any checks whether a method is possible on a variable, such as variable.toLowerCase(), can only be done when we have the actual value, that is, when we run the code. Once more, this is quite fine for small-sized applications, but it can become tedious for large-scale applications. In contrast, strongly typed languages enforce the type rules for both the values and the variables they live in. This means that the language itself can detect the error as you are typing the code, as it has more information about what is going on in your code.

So, in a large software product, (in most cases) we don't want variables that have values of different types. So, we want to be able to somehow say "this variable has to be a number, and if someone tries to put something that is not a number inside it, issue an error."

This is where TypeScript, as a strongly typed language, comes in. We have two ways that we can use to bind a variable to a type. The simpler one is to simply annotate the variable with the type we want it to be, like this:

let variable: number;

The : number part of the code is called a type annotation, and we're doing just that – saying "this variable has to be a number, and if someone tries to put something that is not a number inside it, issue an error."

Now, if we try to assign a number to that variable, everything is fine. But the minute we try to assign a string to the variable, we'll get an error message:

Figure 1.1: Error message from assigning an incorrect type

Figure 1.1: Error message from assigning an incorrect type

This type of annotation is explicit and specific to TypeScript. Another way is simply to assign a value to a variable and let TypeScript work its magic. The magic is called type inference, and that means that TypeScript will try to guess the type of the variable based on the value provided.

Let's define a variable and initialize it with a value, like this:

let variable = 3;

Now, if we try to assign a string to that variable, TypeScript will issue an error:

Figure 1.2: Error message from assigning an incorrect type

Figure 1.2: Error message from assigning an incorrect type

From the error message, we can see the type that TypeScript correctly inferred for the variable – number. Actually, in most cases, we won't even need to add type annotations, as TypeScript's powerful type inference engine will correctly infer the type of the variable.

TypeScript and Functions

Another huge benefit of TypeScript is automatic function invocation checking. Let's say that we have the function we used for our first TypeScript file:

function add (x, y) {

    return x + y;

}

Even without any type annotations, TypeScript still has some information about this function – namely, that it takes two, and exactly two, parameters.

In contrast, JavaScript does not enforce that the number of actual arguments has to conform to the number of parameters defined, so all of the following invocations are valid calls in JavaScript:

add(1, 2); // two arguments

add(1, 2, 3); // three arguments

add(1); // one argument

add(); // no arguments

In JavaScript, we can call a function with more arguments than parameters, fewer arguments, or even without any arguments at all. If we have more arguments than needed, the extra arguments are simply ignored (and stored in the magical arguments variable), and if we have fewer arguments than needed, the extra parameters are given the value undefined. So, in essence, the preceding calls will be correspondingly transformed into the following:

add(1, 2); // no changes, as the number of arguments match the number of parameters.

add(1, 2); // the third argument is ignored

add(1, undefined); // the second parameter is given a value of undefined

add(undefined, undefined); // both parameters are given a value of undefined

In the third and fourth cases, the return value of the function will be the special numeric value NaN.

TypeScript has a radically different approach to this issue. A function can only be called using valid arguments – both in number and in type. So, if we write the same code, but this time in a TypeScript file, we'll get appropriate error messages. For a case where we have extra arguments, we'll get an error message on the extra arguments:

Figure 1.3: Error message from using an incorrect number of 
arguments – too many in this case

Figure 1.3: Error message from using an incorrect number of arguments – too many in this case

For cases with too few arguments, we get the error message on the method itself:

Figure 1.4: Error message from using an incorrect number 
of arguments – too few in this case

Figure 1.4: Error message from using an incorrect number of arguments – too few in this case

In this case, we're notified that a required parameter is missing, as well as what the name and the type of that parameter should be. Note that it's a common JavaScript technique to have methods that accept a variable number of parameters, accept optional parameters, or provide some defaults if a parameter is not specified. All those cases (and many more) are correctly handled by TypeScript.

Note

Details on how to write such methods using TypeScript are inlcuded in Chapter 3, Functions.

Of course, parameter checking works not only on the number but also on the type of the parameters as well. We would want the add function to work only with numbers – it does not make sense to add a Boolean and an object, for example. In TypeScript, we can annotate our function like this:

function add (x: number, y: number) {

    return x + y;

}

This will cause the compiler not only to check that the number of arguments matches the number of parameters but also to verify that the types used for the arguments are actually valid. Since JavaScript can't check for that, adding a Boolean and an object is actually a valid call to the JavaScript equivalent of our add method. Furthermore, since JavaScript tries to be as forgiving as possible, we won't even get a runtime error – the call will be successful, as JavaScript will coerce both the object and Boolean to a common string representation, and then try (and succeed) to add those two values together.

Let's interpret the following call to our function as both JavaScript and TypeScript:

const first = { property: 'value'};

const second = false;

const result = add(first, second);

This is valid, albeit nonsensical, JavaScript code. If run, it will yield the result [object Object]false, which would not be useful in any context.

The same code, interpreted as TypeScript, will yield the following compile type error:

Figure 1.5: Error message on VS Code

Figure 1.5: Error message on VS Code

We can also annotate the return type of the function, adding a type annotation after the parameter list:

function add (x: number, y: number): number {

    return x + y;

}

That is usually not necessary, as TypeScript can actually infer the return type from the return statements given. In our case, since x and y are numbers, x+y will be a number as well, which means that our function will return a number. However, if we do annotate the return type, TypeScript will enforce that contract as well:

Figure 1.6: TypeScript enforcing the correct type

Figure 1.6: TypeScript enforcing the correct type

In either case, whether we explicitly annotate the return type or it's inferred, the type of the function will be applied to any values that are produced by calling the function. So, if we assign the return value to some variable, that variable will have the type of number as well:

Figure 1.7: VS Code showing the type of the variable

Figure 1.7: VS Code showing the type of the variable

Also, if we try to assign the return value to a variable that is already known to be something else other than a number, we'll get an appropriate error:

Figure 1.8: Error message on VS Code

Figure 1.8: Error message on VS Code

Let's make another great point about TypeScript and its type system. As can be seen, the screenshots in this chapter don't show actual compiler error messages – they are taken from inside a code editor (VS Code, an editor that is itself written in TypeScript).

We did not even have to actually compile the code. Instead, we got the error messages while we typed the code – an experience that is familiar to developers in other strongly typed languages, such as C# or Java.

This happens because of the design of the TypeScript compiler, specifically its Language Service API. This enables the editor to easily use the compiler to check the code as it's written so that we can get a nice and intuitive GUI. Additionally, since all the editors will use the same compiler, the development experience will be similar across different editors. This is a dramatic change from the situation that we started with – fully writing, loading, and actually executing the JavaScript code in order to know whether it even makes sense.

Note

In recent years, some editors have started using the TypeScript Language Service API on JavaScript code as well, so TypeScript improves even the plain JavaScript development experience.

In a nutshell, using TypeScript changes one of the most prevalent pain points for JavaScript development – inconsistent and sometimes even impossible tooling support – into a much easier and more convenient experience. In our case, we need only to open a parenthesis when calling the add function, and we'll see the following:

Figure 1.9: List of parameters that the function can take

Figure 1.9: List of parameters that the function can take

We are shown a list of parameters that shows that the function – which can be defined in another file, by another developer – takes two numbers and also returns a number.

Exercise 1.02: Working with Functions in TypeScript

In this exercise, we'll define a simple function and see how we can and can't invoke it. The function we will be developing will be a string utility function that shortens a string to a snippet. We'll basically cut off the text after a given length, but take care that we don't chop a word in half. If the string is larger than the maximum length, we'll add an ellipsis () to the end:

Note

The code files for this exercise can be found here: https://packt.link/BHj53.

  1. Create a new file called snippet.ts.
  2. In snippet.ts, define a simple function called snippet:

    function snippet (text: string, length: number) : string {

  3. Check whether the text is smaller than the specified length, and if it is, return it unchanged:

        if (text.length < length) {

            return text;

        }

  4. If the text is larger than the maximum length, we'll need to add an ellipsis. The maximum number of characters that we'll be able to show is the specified length minus the length of our ellipsis (as it takes up space too). We'll use the slice string method to extract that many characters from the text:

        const ellipsis = "...";

        let result = text.slice(0, length - ellipsis.length);

  5. We'll find the last word boundary before the cutoff, using lastIndexOf, and then combine the text up to that point with the ellipsis:

        const lastSpace = result.lastIndexOf(" ");

        result = `${result.slice(0, lastSpace)}${ellipsis}`;

  6. Return the result from the function:

        return result;

    }

  7. After the function, create a few calls to the function with different parameter types:

    // correct call and usage

    const resultOne = snippet("TypeScript is a programming language that is a strict syntactical superset of JavaScript and adds optional static typing to the language.", 40);

    console.log(resultOne);

    // missing second parameter

    const resultTwo = snippet("Lorem ipsum dolor sit amet");

    console.log(resultTwo);

    // The first parameter is of incorrect type

    const resultThree = snippet(false, 40);

    console.log(resultThree);

    // The second parameter is of incorrect type

    const resultFour = snippet("Lorem ipsum dolor sit amet", false);

    console.log(resultFour);

    // The result is assigned to a variable of incorrect type

    var resultFive: number = snippet("Lorem ipsum dolor sit amet", 20);

    console.log(resultFive);

  8. Save the file and run the following command in the folder:

    tsc snippet.ts

  9. Verify that the file did not compile correctly. You will get specifics from the compiler about the errors found, and the compilation will end with the following message:

    Found 3 errors.

  10. Comment out or delete all invocations except the first one:

    // correct call and usage

    var resultOne = snippet("TypeScript is a programming language that is a strict syntactical superset of JavaScript and adds optional static typing to the language.", 40);

    console.log(resultOne);

  11. Save the file and compile it again:

    tsc snippet.ts

  12. Verify that the compilation ended successfully and that there is a snippet.js file generated in the same folder. Execute it in the node environment with the following command:

    node snippet.js

    You will see an output that looks as follows:

    TypeScript is a programming language...

In this exercise, we developed a simple string utility function, using TypeScript. We saw the two main strengths of TypeScript. For one, we can see that the code is idiomatic JavaScript – we could leverage our existing JavaScript knowledge to write the function. Steps 3 through 6, the actual body of the function, are exactly the same in JavaScript and TypeScript.

Next, we saw that TypeScript takes care that we invoke the function correctly. In step 7, we tried five different invocations of the function. The last four invocations are incorrect ones – they would have been errors either in JavaScript or TypeScript. The important difference is that with TypeScript, we immediately got feedback that the usage is invalid. With JavaScript, the errors would have only been visible when we, or a client, actually executed the code.

TypeScript and Objects

One great thing about JavaScript is its object literal syntax. While in some languages, to create an object we have to do a lot of groundwork, such as creating classes and defining constructors, in JavaScript, and by extension in TypeScript, we can just create the object as a literal. So, if we want to create a person object, with firstName and lastName properties, we only need to write the following:

const person = {

    firstName: "Ada",

    lastName: "Lovelace"

}

JavaScript makes it easy to create and use the object, just like any other value. We can access its properties, pass it as an argument into methods, receive it as a return value from functions, and so on. And because of JavaScript's dynamic nature, it's very easy to add properties to our object. If we wanted to add an age property to our object, we could just write the following:

person.age = 36;

However, because of the loose typing, JavaScript has no knowledge of our object. It does not know what the possible properties of our object are, and what methods can and cannot use it as an argument or a return value. So, say we make a typo, for example, writing out something like this:

console.log("Hi, " + person.fristName);

JavaScript will happily execute this code and write out Hi undefined. That is not what we intended, and will only be visible and detectible when the code is actually run in the browser. Using TypeScript, we have a few options to remedy that. So, let's rewrite our person object using TypeScript:

const person = {

    firstName: "Ada",

    lastName: "Lovelace"

}

console.log(`Hi, ${person.fristName}`);

This code will immediately be marked as invalid by the compiler, even when we haven't added any type information:

Figure 1.10: TypeScript compiler inferring the type of the object

Figure 1.10: TypeScript compiler inferring the type of the object

From the error message, we can see what the TypeScript compiler inferred for the type of our object – it thinks that its type consists of two properties, firstName of type string and lastName of type string. And according to that definition, there is no place for another property called fristName, so we are issued an error.

Note

Notice the suggestion Did you mean 'firstName'? along with the link to the definition of the person class. Since typos are common, the type inference algorithm tries to detect and offer suggestions on common typos.

So, once more, we have detected a bug in our code just by using TypeScript, with no additional code written. TypeScript does this by analyzing the definition of the object and extracts the data from there. It will allow us to write code such as the following:

person.lastName = "Byron";

But it will not allow us to write code where we set lastName to a number:

Figure 1.11: Error message by assigning an incorrect type to lastName

Figure 1.11: Error message by assigning an incorrect type to lastName

Sometimes, we know more about the shape of our objects than TypeScript does. For example, TypeScript inferred that our type has only the firstName and lastName properties. So, if we set the age in TypeScript, with person.age = 36;, we will get an error. In this case, we can explicitly define the type of our object, using a TypeScript interface. The syntax that we can use looks as follows:

interface Person {

    firstName: string;

    lastName: string;

    age? : number;

}

With this piece of code, we're defining an abstract – a structure that some object will need to satisfy in order to be allowed to be treated as a Person object. Notice the question mark (?) next to the age variable name. That denotes that that property is in fact optional. An object does not have to have an age property in order to be a Person object. However, if it does have an age property, that property has to be a number. The two other properties (firstName and lastName) are mandatory.

Using this definition, we can define and use our object using the following:

const person: Person = {

    firstName: "Ada",

    lastName: "Lovelace"

}

person.age = 36;

We can use interfaces as type annotations for function arguments and return types as well. For example, we can define a function called showFullName that will take a person object and display the full name to the console:

function showFullName (person: Person) {

    console.log(`${person.firstName} ${person.lastName}`)

}

If we invoke this function with showFullName(person), we'll see that it will display Ada Lovelace on the console. We can also define a function that will take two strings, and return a new object that fits the Person interface:

function makePerson (name: string, surname: string): Person {

    const result = {

        firstName: name,

        lastName: surname

    }

    return result;

}

const babbage = makePerson("Charles", "Babbage");

showFullName(babbage);

One important thing that we need to point out is that, unlike in other languages, the interfaces in TypeScript are structural and not nominal. What that means is that if we have a certain object that fulfills the "rules" of the interface, that object can be considered to be a value of that interface. In our makePerson function, we did not specify that the result variable is of the Person type – we just used an object literal with firstName and lastName properties, which were strings. Since that is enough to be considered a person, the code compiles and runs just fine. This is a huge boon to the type inference system, as we can have lots of type checks without having to explicitly define them. In fact, it's quite common to omit the return type of functions.

Exercise 1.03: Working with Objects

In this exercise, we'll define a simple object that encapsulates a book with a few properties. We'll try to access and modify the object's data and verify that TypeScript constrains us according to inferred or explicit rules. We will also create a function that takes a book object and prints out the book's details:

Note

The code files for this exercise can be found here: https://packt.link/N8y1f.

  1. Create a new file called book.ts.
  2. In book.ts, define a simple interface called Book. We will have properties for the author and the title of the book, optional properties for the number of pages of the book, and a Boolean that denotes whether we have read the book:

    interface Book {

        author: string;

        title: string;

        pages?: number;

        isRead?: boolean;

    }

  3. Add a function called showBook that will display the book's author and title to the console. It should also display whether the book has been read or not, that is, whether the isRead property is present:

    function showBook(book: Book) {

        console.log(`${book.author} wrote ${book.title}`);

        if (book.isRead !== undefined) {

            console.log(` I have ${book.isRead ? "read" : "not read"} this book`);

        }

    }

  4. Add a function called setPages that will take a book and a number of pages as parameters, and set the pages property of the book to the provided value:

    function setPages (book: Book, pages: number) {

        book.pages = pages;

    }

  5. Add a function called readBook that will take a book and mark it as having been read:

    function readBook(book: Book) {

        book.isRead = true;

    }

  6. Create several objects that fulfill the interface. You can, but don't have to, annotate them with the interface we have created:

    const warAndPeace = {

        author: "Leo Tolstoy",

        title: "War and Peace",

        isRead: false

    }

    const mobyDick: Book = {

        author: "Herman Melville",

        title: "Moby Dick"

    }

  7. Add code that will call methods on the books:

    setPages(warAndPeace, 1225);

    showBook(warAndPeace);

    showBook(mobyDick);

    readBook(mobyDick);

    showBook(mobyDick);

  8. Save the file and run the following command in the folder:

    tsc book.ts

  9. Verify that the compilation ended successfully and that there is a book.js file generated in the same folder. Execute it in the node environment with the following command:

    node book.js

    You will see an output that looks as follows:

    Leo Tolstoy wrote War and Peace

      I have not read this book

    Herman Melville wrote Moby Dick

    Herman Melville wrote Moby Dick

      I have read this book

In this exercise, we created and used an interface, a purely TypeScript construct. We used it to describe the shape of the objects we will use. Without actually creating any specific objects of that shape, we were able to use the full power of TypeScript's tooling and type inference to create a couple of functions that operate on the objects of the given shape.

After that, we were able to actually create some objects that had the required shape (with and without making the declaration explicit). We were able to use both kinds of objects as parameters to our functions, and the results were in line with the interface we declared.

This demonstrated how a simple addition of an interface made our code much safer to write and execute.

Basic Types

Even though JavaScript is a loosely typed language, that does not mean that values do not have types. There are several primitive types that are available to the JavaScript developer. We can get the type of the value using the typeof operator, available both in JavaScript and TypeScript. Let's inspect some values and see what the results will be:

const value = 1234;

console.log(typeof value);

The execution of the preceding code will write the string "number" to the console. Now, consider another snippet:

const value = "textual value";

console.log(typeof value);

The preceding expression will write the string "string" to the console. Consider the following snippet:

const value = false;

console.log(typeof value);

This will write out "boolean" to the console.

All of the preceding types are what are called "primitives." They are baked directly into the execution environment, whether that is a browser or a server-side application. We can always use them as needed. There is an additional primitive type that has only a single value, and that's the undefined type, whose only value is undefined. If we try to call typeof undefined, we will receive the string "undefined". Other than the primitives, JavaScript and by extension TypeScript have two so-called "structural" types. Those are, respectively, objects, that is, custom-created pieces of code that contain data, and functions, that is, custom-created pieces of code that contain logic. This distinction between data and logic is not a clear-cut border, but it can be a useful approximation. For example, we can define an object with some properties using the object literal syntax:

const days = {

    "Monday": 1,

    "Tuesday": 2,

    "Wednesday": 3,

    "Thursday": 4,

    "Friday": 5,

    "Saturday": 6,

    "Sunday": 7,

}

Calling the typeof operator on the days object will return the string "object". We can also use the typeof operator if we have an add function as we defined before:

function add (x, y) {

    return x + y;

}

console.log(typeof add);

This will display the string "function".

Note

Recent versions of JavaScript added bigint and symbol as primitive types, but they won't be encountered outside of specific scenarios.

Exercise 1.04: Examining typeof

In this exercise, we'll see how to use the typeof operator to determine the type of a value, and we will investigate the responses:

Note

The code files for this exercise can be found here: https://packt.link/uhJqN.

  1. Create a new file called type-test.ts.
  2. In type-test.ts, define several variables with differing values:

    const daysInWeek = 7;

    const name = "Ada Lovelace";

    const isRaining = false;

    const today = new Date();

    const months = ["January", "February", "March"];

    const notDefined = undefined;

    const nothing = null;

    const add = (x:number, y: number) => x + y;

    const calculator = {

        add

    }

  3. Add all the variables into a containing array, using the array literal syntax:

    const everything = [daysInWeek, name, isRaining, today, months, notDefined, nothing, add, calculator];

  4. Loop all the variables using a for..of loop, and for each value, call the typeof operator. Show the result on the console, along with the value itself:

    for (const something of everything) {

        const type = typeof something;

        console.log(something, type);

    }

  5. Save the file and run the following command in the folder:

    tsc type-test.ts

  6. After the compilation is done, you will have a type-test.js file. Execute it in the node environment with the following command:

    node type-test.js

    You will see that the output is as follows:

    7 number

    Ada Lovelace string

    false boolean

    2021-04-05T09:14:56.259Z object

    [ 'January', 'February', 'March' ] object

    undefined undefined

    null object

    [Function: add] function

    { add: [Function: add] } object

Note specifically the output from the months and nothing. typeof variables will return the string "object" both for arrays and the null value. Also note that the calculator variable is an object whose only property is actually a function; that is, we have an object whose piece of data is actually a piece of logic. This is possible because functions are first-class values in JavaScript and TypeScript, which means that we can manipulate them just like we would regular values.

Strings

Words and text are part of any application, just as they are part of everyday life. In JavaScript, they are represented by the string type. Unlike in other languages, such as C++ or Java, strings in JavaScripts are not treated as an array-like object that consists of smaller parts (characters). Instead, strings are a first-order citizen of JavaScript. In addition, JavaScript strings natively support Unicode, so we won't get any problems with characters with, for example, Cyrillic or Arabic script. Just like in JavaScript, to define a string in TypeScript, we can use single quotes (') or double quotes ("). Of course, if we start the string with a single quote, we have to end it with a single quote, and vice versa. We can also use a special type of string definition, called template strings. These strings are delimited with the backtick character (`) and support two very important things for web development – newlines and embedded expressions. They are supported in all environments that support ES2015, but TypeScript is able to compile to any JavaScript target environment.

Using embedded expressions and newlines inside a string enables us to generate nice HTML, because instead of string concatenation, we're able to use embedded expressions to have a much clearer view of the generated output. For example, if we had a person object with firstName and lastName properties, and we wanted to display a simple greeting inside a <div> tag, we would have to write code as follows:

const html = "<div class="greeting"> Hello, " + firstName + " " + lastName + " </div>";

From this code (which can get much more complex), it's difficult to see what will actually be written and where. Using template strings transforms this into the following:

const html = `<div class="greeting">

    Hello, ${firstName} ${lastName}

</div>";

In order to output the firstName and lastName values, we have to surround them with brackets ({}), preceded by a dollar sign ($). We are not limited to variable names, but can have whole expressions, including the conditional operator (?:).

Numbers

Numbers are an important aspect of the world. We use them to quantify everything around us. And, it's worth noting, that there are two quite different kinds of numbers that you encounter in your daily life – integers and real numbers. One distinguishing difference between the two kinds of numbers is that integers are numbers without any fractional part. These often result from counting things; for example, the number of people in town. On the other hand, real numbers can have a fractional component to them. For example, the weight or height of a person is often a real number.

In most programming languages, these two types of numbers are represented with (at least) two different primitive types; for example, in C#, we have a type called int for integers and a type called float for real numbers.

In JavaScript, and consequently in TypeScript, they are indeed the same primitive type. That primitive type is simply called number. Under the hood, it's a 64-bit floating-point number, fully implementing the IEEE 754 standard. This standard is specified for real numbers, and this leads to some weirdness that is specific to JavaScript. For example, in most environments, dividing by zero results in an error. In JavaScript and TypeScript, division by zero results in some special numbers such as Infinity or NaN. Additionally, there is no concept of integer division in JavaScript, as division is always done using real numbers.

However, even if everything is stored as floating-point real numbers, JavaScript guarantees that all operations that can be done using only integer arithmetic will be done exactly. One famous example of this behavior is adding 0.1 to 0.2. In all compliant JavaScript engines, we get the result 0.30000000000000004 because of the finite precision of the underlying type. What we are guaranteed is that we can never get a decimal result if we are adding integers. The engine makes sure that 1+1=2 with no decimal remainder. All integer operations are completely safe, but only if the results are within a specified range. JavaScript has a special constant defined (Number.MAX_SAFE_INTEGER) with a value of 9007199254740991 (with digit grouping, this is represented as 9.007.199.254.740.991) over which we might get precision and rounding errors.

Booleans

Booleans are one of the simplest, and also one of the most used and useful, primitive types. This datatype has exactly two values, true and false. The useful thing is that if a variable of this type does not have a certain value, well, then it automatically has the other, as that is the only other possible option. In theory, this is sound, but in JavaScript, there are a lot of possibilities for things to go wrong. Since it has no type information, it cannot guarantee that a certain variable actually holds a Boolean value, which means that we always have to be careful of our Boolean checks.

TypeScript completely defines away this problem. Say we define a variable as a Boolean, using either a type annotation or type inference, as follows:

let isRead = false;

We can be absolutely sure that the variable will always have exactly one of the two possible values.

Arrays

One of the reasons computers are popular, aside from accessing social networking sites and playing video games, is that they are able to run the same processing algorithm on a whole collection of values, as many times as needed, without getting bored or making any errors. In order to be able to do that, we need to somehow organize the data into a collection of similar values that we can access one at a time. In JavaScript, the primary mechanism for such processing is the array. JavaScript has an extremely simple interface for creating arrays using the array literal syntax. We just list the elements, surrounded by brackets ([ ]), and we have an array:

const numbers = [1, 2, 3, 4, 5];

We can access that array using an index:

console.log(numbers[3]) // writes out 4, as arrays in JavaScript are //…0-based

numbers[1] = 200; // the second element becomes 200

That makes it easy to use a for loop to go through the elements and process them all with a single piece of code:

for (let index = 0; index < numbers.length; index += 1) {

    const element = numbers[index];

    console.log(`The element at index ${index} has a value of ${element}`);

}

We can also use a for..of loop to iterate through the values, and the following snippet will calculate the sum of all the numbers in the array:

let sum = 0;

for (const element of numbers) {

    sum += element;

}

As with anything in JavaScript, it has no mechanism to enforce that all the items in an array satisfy the "similarity" requirement we mentioned previously. So, there's nothing stopping us from adding a string, a Boolean, an object, or even a function to the array of numbers we have defined. All of these are valid JavaScript commands that will execute successfully:

numbers[1] = false;

numbers[2] = new Date();

numbers[3] = "three";

numbers[4] = function () {

    console.log("I'm really not a number");

};

In almost all cases, it is not to our benefit to have an array with vastly different types as elements. The main benefit of arrays is that we can group similar items together and work with all of them with the same code. If we have different types, we lose that advantage, so we might as well not use an array at all.

With TypeScript, we can restrict the type so that an array will only allow a single type of value for its elements. Arrays have something that is referred to as a composite or generic type. That means that when we are specifying the type of the array, we're specifying it indirectly, via another type.

In this case, we define the type of the array through the type of the array's elements, for example, we can have an array whose elements will be numbers or an array whose elements will be strings. In TypeScript, we denote that by writing the type of the element and then appending brackets to the type name. So, if we needed our numbers array to only accept values whose type is number, we will denote that as follows:

let numbers: number[];

Even better, if we are initializing our array, we can omit the type annotation and let TypeScript infer the value:

Figure 1.12: TypeScript inferring the type of the elements in the array

Figure 1.12: TypeScript inferring the type of the elements in the array

As shown previously, TypeScript will not let us use the push method with a value whose type does not match the type of the elements, nor will it allow elements to be set to invalid values.

Another, equivalent way to denote the type of the array is to use generic type syntax. In that case, we can use the Array type, with the type of the actual elements in angle brackets:

let numbers: Array<number>;

Generic classes and methods will be covered in detail in Chapter 9, Generics and Conditional Types.

The benefit here is that we can be certain that if an array claims to have elements of a certain type, it will indeed have that kind of element, and we can process them without worrying that a bug introduced an incompatible element.

Tuples

Another common usage of arrays in JavaScript is to group data – just like objects, but without the hassle (and benefit) of property names. We could, for example, instead of creating a person object create a person array where, by convention, we'll use the first element to hold the first name, the second element to hold the last name, and the third element to hold the age. We could define such an array using the following:

const person = ["Ada", "Lovelace", 36];

console.log(`First Name is: ${person[0]}`);

console.log(`Last Name is: ${person[1]}`);

console.log(`Age is: ${person[2]}`);

In this case, even as we are using the same structure – an array – we're not using it to group an unknown number of unrelated data of the same type, we're using it to group a known number of related data that can be of separate types. This kind of array is called a tuple. Once more, JavaScript has no mechanism to enforce the structure of a tuple, so in our code we can do lots of things that are syntactically valid, but nonsensical semantically. We could add a fourth element in the array, we can set the first element to be a number, the third to be a function, and so on.

With TypeScript, we can formally define the number and types of the data elements that we need inside a tuple, using syntax such as the following:

const person: [string, string, number] = ["Ada", "Lovelace", 36];

The [string, string, number] declaration tells TypeScript that we intend to use a tuple of three elements, that the first two elements will be a string, and the third will be a number. TypeScript now has enough information to enforce the structure. So, if we write code that will call the toLowerCase method on the first element of the tuple and multiply the third element by 10, that will work, as the first operation is valid on a string and the second is valid on a number:

console.log(person[0].toLowerCase());

console.log(person[2] * 10);

But if we try the operations the other way around, we'll get errors on both calls:

Figure 1.13: TypeScript error when performing incorrect operations

Figure 1.13: TypeScript error when performing incorrect operations

Additionally, if we try to access an element that is outside of the defined range, we'll get an error as well:

Figure 1.14: TypeScript when accessing elements outside the defined range

Figure 1.14: TypeScript when accessing elements outside the defined range

Schwartzian transform

Arrays have a helpful sort function, which we can use to sort the objects contained in the array. However, during the sorting process, multiple comparisons will be done on the same objects. For example, if we sort an array of 100 numbers, the method that compares two numbers will be called more than 500 times, on average. Let's say that we have a Person interface, defined with the following:

interface Person {

    firstName: string;

    lastName: string;

}

If we want to get the full name of the person, we might use a function such as this:

function getFullName (person: Person) {

    return `${person.firstName} ${person.lastName}`;

}

If we have an array of Person objects, called persons, and want to sort it according to full name, we might use the following code:

persons.sort((first, second) => {

    const firstFullName = getFullName(first);

    const secondFullName = getFullName(second);

    return firstFullName.localeCompare(secondFullName);

})

This will sort the persons array, albeit in an inefficient manner. If we have 100 Person objects, this means that we have 100 different targets for the getFullName functions. But if we have more than 500 calls to the comparison function, that would mean that we have more than 1,000 calls to the getFullName function, so at least 900 calls are redundant.

Note

The relation gets worse: for 10,000 persons, we will have around a quarter of a million redundant calls.

Our method is fast and trivial, but if some expensive calculations were needed, simple sorting could slow down our application.

Fortunately, there's a simple technique called a Schwartzian transform that can help us with that. The technique has three parts:

  • We will transform each element in the array into a tuple of two elements. The first element of the tuple will be the original value, and the second will be the result of the ordering function (colloquially, the Schwartz).
  • We will sort the array on the second element of the tuple.
  • We will transform each tuple, discarding the ordering element and taking the original value.

We will employ this technique in the following exercise.

Exercise 1.05: Using Arrays and Tuples to Create an Efficient Sort of Objects

In this exercise, we are going to employ the Schwartzian transform to sort and print a predefined array of programmers. Each programmer object will be an instance of the Person interface, defined in the previous section.

We'll want to sort the programmers based on their full name, which can be calculated using the getFullName function, also from the previous section.

In order to implement a Schwartzian transform, we'll take the following steps:

We'll use the map method of the array in order to transform our programmers into a tuple of the [Person, string] type, where the first element is the actual programmer and the second element is the full name string.

We'll use the sort method of the array to sort the tuples, using the second element of each tuple.

We'll use the map method once more to transform the tuples back to an array of programmers by just taking the first element and discarding the second element.

Let's start:

Note

The code files for this exercise can be found here: https://packt.link/EgZnX.

  1. Create a new file called person-sort.ts.
  2. Inside the file, create the interface for the Person objects:

    interface Person {

        firstName: string;

        lastName: string;

    }

  3. Create the function that will get the full name of a given person:

    let count = 0;

    function getFullName (person: Person) {

        count += 1;

        return `${person.firstName} ${person.lastName}`;

    }

    We will use the count variable to detect the total number of calls of the function.

  4. Define an array of persons and add a few objects with firstName and lastName properties:

    const programmers: Person[] = [

        { firstName: 'Donald', lastName: 'Knuth'},

        { firstName: 'Barbara', lastName: 'Liskow'},

        { firstName: 'Lars', lastName: 'Bak'},

        { firstName: 'Guido', lastName: 'Van Rossum'},

        { firstName: 'Anders', lastName: 'Hejslberg'},

        { firstName: 'Edsger', lastName: 'Dijkstra'},

        { firstName: 'Brandon', lastName: 'Eich'},

        // feel free to add as many as you want

    ];

  5. Define a naïve and straight forward sorting function:

    // a naive and straightforward sorting function

    function naiveSortPersons (persons: Person[]): Person[] {

        return persons.slice().sort((first, second) => {

            const firstFullName = getFullName(first);

            const secondFullName = getFullName(second);

            return firstFullName.localeCompare(secondFullName);

        })

    }

  6. Use a Schwartzian transform and define a function that will take an array of persons and return (a sorted) array of persons:

    function schwartzSortPersons (persons: Person[]): Person[] {

  7. Use the array's map function to transform each element into a tuple:

        const tuples: [Person, string][] = persons.map(person => [person, getFullName(person)]);

  8. Sort the tuples array of tuples, using the standard sort method:

        tuples.sort((first, second) => first[1].localeCompare(second[1]));

    We should note that the sort function takes two objects, in our case, two tuples, and we sort the tuples according to their second element – the result of the getFullName call.

  9. Transform the sorted array of tuples into the format we want – just an array of person objects – by taking the first element of each tuple, discarding the Schwartz:

        const result = tuples.map(tuple => tuple[0]);

  10. The last three steps are the three parts of the Schwartzian transform.
  11. Return the new array from the function:

        return result;

    }

  12. Add a line that will call the naiveSortPersons function on our defined array:

    count = 0;

    const sortedNaive = naiveSortPersons(programmers);

  13. Output both the sorted array, and the count variable.

    console.log(sortedNaive);

    console.log(`When called using the naive approach, the function was called ${count} times`);

  14. Add a line that will call the schwartzSortPersons function on our defined array:

    count = 0;

    const sortedSchwartz = schwartzSortPersons(programmers);

  15. Output both the sorted array and the count variable. The count variable should be identical to the number of items in the array, which is 7 in our example. Without the optimization, the method would have been called 28 times:

    console.log(sortedSchwartz);

    console.log(`When called using the Schwartzian transform approach, the function was called ${count} times`);

  16. Save and compile the file:

    tsc person-sort.ts

  17. Verify that the compilation ended successfully and that there is a person-sort.js file generated in the same folder. Execute it in the node environment with the following command:

    node person-sort.js

    You will see an output that looks as follows:

    [

      { firstName: 'Anders', lastName: 'Hejslberg' },

      { firstName: 'Barbara', lastName: 'Liskow' },

      { firstName: 'Brandon', lastName: 'Eich' },

      { firstName: 'Donald', lastName: 'Knuth' },

      { firstName: 'Edsger', lastName: 'Dijkstra' },

      { firstName: 'Guido', lastName: 'Van Rossum' },

      { firstName: 'Lars', lastName: 'Bak' }

    ]

    When called using the naive approach, the function was called 28 times

    [

      { firstName: 'Anders', lastName: 'Hejslberg' },

      { firstName: 'Barbara', lastName: 'Liskow' },

      { firstName: 'Brandon', lastName: 'Eich' },

      { firstName: 'Donald', lastName: 'Knuth' },

      { firstName: 'Edsger', lastName: 'Dijkstra' },

      { firstName: 'Guido', lastName: 'Van Rossum' },

      { firstName: 'Lars', lastName: 'Bak' }

    ]

    When called using the Schwartzian transform approach, the function was called 7 times

We can easily check that the values that are outputted are sorted according to their full names. We can also notice a 7 at the end of output – that's the total number of calls of the getFullName function. Since we have 7 items in the programmers array, we can conclude that the function was called just once for each object.

We could have instead sorted the programmers array directly, using code such as the following:

programmers.sort((first, second) => {

    const firstFullName = getFullName(first);

    const secondFullName = getFullName(second);

    return firstFullName.localeCompare(secondFullName);

});

console.log(count);

In this case, for this array, the count of execution of the getFullName function would have been 28, which is four times as high as our optimized version.

Enums

Often we have some types that have a predefined set of values, and no other value is valid. For example, there are four and only four cardinal directions (East, West, North, and South). There are four and only four different suits in a deck of cards. So, how do we define a variable that should have such a value?

In TypeScript, we can use an enum type to do that. The simplest way to define an enum would be as follows:

enum Suit {

    Hearts,

    Diamonds,

    Clubs,

    Spades

}

We can then define and use a variable of such type, and TypeScript will help us use it:

let trumpSuit = Suit.Hears;

TypeScript will infer that the type of the trumpSuit variable is Suit and will only allow us to access those four values. Any attempt to assign something else to the variable will result in an error:

Figure 1.15: TypeScript inferring the type of trumpSuit

Figure 1.15: TypeScript inferring the type of trumpSuit

So far, all the types we've encountered were JavaScript types that were augmented with TypeScript. Unlike that, enums are specific to TypeScript. Under the hood, the Suit class actually compiles into an object with values like this:

{

  '0': 'Hearts',

  '1': 'Diamonds',

  '2': 'Clubs',

  '3': 'Spades',

  Hearts: 0,

  Diamonds: 1,

  Clubs: 2,

  Spades: 3

}

TypeScript will automatically assign numbers starting with zero to the options provided and add a reverse mapping as well, so if we have the option, we can get the value, but if we have the value, we can map to the option as well. We can also explicitly set the provided numbers as well:

enum Suit {

    Hearts = 10,

    Diamonds = 20,

    Clubs = 30,

    Spades = 40

}

We can also use strings instead of numbers, with syntax like this:

enum Suit {

    Hearts = "hearts",

    Diamonds = "diamonds",

    Clubs = "clubs",

    Spades = "spades"

}

These enums are called string-based enums, and they compile to an object like this:

{

  Hearts: 'hearts',

  Diamonds: 'diamonds',

  Clubs: 'clubs',

  Spades: 'spades'

}

Any and Unknown

So far, we have explained how TypeScript inference works, and how powerful it is. But sometimes we actually want to have JavaScript's "anything goes" behavior. For example, what if we genuinely need a variable that will sometimes hold a string and sometimes hold a number? The following code will issue an error because we're trying to assign a string to a variable that TypeScript inferred to be a number:

let variable = 3;

if (Math.random()>0.5) {

    variable = "not-a-number";

}

This is how the code will appear on VS Code with the error message:

Figure 1.16: TypeScript inferring the type of variable

Figure 1.16: TypeScript inferring the type of variable

What we need to do is somehow suspend the type inference for that specific variable. To be able to do that, TypeScript provides us with the any type:

let variable: any = 3;

if (Math.random()>0.5) {

    variable = "not-a-number";

}

This type annotation reverts the variable variable to the default JavaScript behavior, so none of the calls involving that variable will be checked by the compiler. Additionally, most calls that include a variable of the any type will infer a result of the same type. This means that the any type is highly contagious, and even if we define it in a single place in our application, it can propagate to lots of places.

Since using any effectively negates most of TypeScript's benefits, it's best used as seldom as possible, and only when absolutely necessary. It's a powerful tool to use the opt-in/opt-out design of TypeScript so that we can gradually upgrade existing JavaScript code into TypeScript.

One scenario that is sometimes used is a combination of the dynamic nature of any and the static nature of TypeScript – we can have an array where the elements can be anything:

const everything: any[] = [ 1, false, "string"];

Starting from version 3.0, TypeScript also offers another type with dynamic semantics – the unknown type. While still dynamic, it's much more constricted in what can be done with it. For example, the following code will compile using any:

const variable: any = getSomeResult(); // a hypothetical function //with some return value we know nothing about

const str: string = variable; // this works, as any might be a //string, and "anything goes";

variable.toLowerCase(); // we are allowed to call a method, //and we'll determine at runtime whether that's possible

On the other hand, the same code with an unknown type annotation results in the following:

Figure 1.17: TypeScript compiler error message

Figure 1.17: TypeScript compiler error message

The unknown type basically flips the assertion and the burden of proof. With any, the flow is that, since we don't know that it's not a string, we can treat it as a string. With unknown, we don't know whether it's a string, so we can't treat it as a string. In order to do anything useful with an unknown, we need to explicitly test its value and determine our actions based on that:

const variable: unknown = getSomeResult(); // a hypothetical function with some return value we know nothing about

if (typeof variable === "string") {

    const str: string = variable; // valid, because we tested if the value inside `variable` actually has a type of string

    variable.toLowerCase();

}

Null and Undefined

One of the specifics of JavaScript is that it has two separate values that signify that there isn't a value: null and undefined. The difference between the two is that null has to be specified explicitly – so if something is null, that is because someone set it to null. Meanwhile, if something has the value undefined usually it means that the value is not set at all. For example, let's look at a person object defined with the following:

const person = {

    firstName: "Ada",

    lastName: null

}

The value of the lastName property has been set to null explicitly. On the other hand, the age property is not set at all. So, if we print them out, we'll see that the lastName property has a value of null, while the age property has a value of undefined:

console.log(person.lastName);

console.log(person.age);

We should note that if we have some optional properties in an object, their default value will be undefined. Similarly, if we have optional parameters in a function, the default value of the argument will be undefined as well.

Never

There is another "not a value" type that's specific to TypeScript, and that is the special never type. This type represents a value that never occurs. For example, if we have a function where the end of the function is not reachable and has no return statements, its return type will be never. An example of such a function will be as follows:

function notReturning(): never {

    throw new Error("point of no return");

}

const value = notReturning();

The type of the value variable will be inferred as never. Another situation where never is useful is if we have a logical condition that cannot be true. As a simple example, let's look at this code:

const x = true;

if (x) {

    console.log(`x is true: ${x.toString()}`);

}

The conditional statement will always be true, so we will always see the text in the console. But if we add an else branch to this code, the value of x inside the branch cannot be true because we're in the else branch, but cannot be anything else because it was defined as true. So, the actual type is inferred to be never. Since never does not have any properties or methods, this branch will throw a compile error:

Figure 1.18: Compiler error from using the never type

Figure 1.18: Compiler error from using the never type

Function Types

The last built-in type in JavaScript that we'll take a look at is not really a piece of data – it's a piece of code. Since functions are first-order objects in JavaScript, they remain so in TypeScript as well. And just like the others, functions get types as well. The type of a function is a bit more complicated than the other types. In order to identify it, we need all the parameters and their types, as well as the return values and their types. Let's take a look at an add function defined with the following:

const add = function (x: number, y: number) {

    return x + y;

}

To fully describe the type of the function, we need to know that it is a function that takes a number as the first parameter and a number as the second parameter and returns a number. In TypeScript, we'll write this as (x: number, y: number) => number.

Making Your Own Types

Of course, aside from using the types that are already available in JavaScript, we can define our own types. We have several options for that. We can use the JavaScript class specification to declare our own classes, with properties and methods. A simple class can be defined with the following:

class Person {

    constructor(public firstName: string, public lastName: string, public age?: number) {

    }

    getFullName() {

        return `${this.firstName} ${this.lastName}`;

    }

}

We can create objects of this class and use methods on them:

const person = new Person("Ada", "Lovelace");

console.log(person.getFullName());

Another way to formalize our complex structures is to use an interface:

interface Person

{

    firstName: string;

    lastName: string;

    age?: string;

}

Unlike classes, which compile to JavaScript classes or constructor functions (depending on the compilation target), interfaces are a TypeScript-only construct. When compiling, they are checked statically, and then removed from the compiled code.

Both classes and interfaces are useful if implementing a class hierarchy, as both constructs are suitable for extension and inheritance.

Yet another way is to use type aliases, with the type keyword. We can basically put a name that we will use as a type alias to just about anything available in TypeScript. For example, if we want to have another name for the primitive number type, for example, integer, we can always do the following:

type integer = number;

If we want to give a name to a tuple, [string, string, number?], that we use to store a person, we can alias that with the following:

type Person = [string, string, number?];

We can also use objects and functions in the definition of a type alias:

type Person = {

    firstName: string;

    lastName: string;

    age?: number;

}

type FilterFunction = (person: Person) => boolean;

We will go into more details and intricacies of the class, interface, and type keywords in Chapter 4, Classes and Objects, Chapter 5, Interfaces and Inheritance, and Chapter 6, Advance Types, respectively.

Exercise 1.06: Making a Calculator Function

In this exercise, we'll define a calculator function that will take the operands and the operation as parameters. We will design it so it is easy to extend it with additional operations and use that behavior to extend it:

Note

The code files for this exercise can be found here: https://packt.link/dKoCZ.

  1. Create a new file called calculator.ts.
  2. In calculator.ts, define an enum with all the operators that we want to support inside our code:

    enum Operator {

        Add = "add",

        Subtract = "subtract",

        Multiply = "multiply",

        Divide = "divide",

    }

  3. Define an empty (for now) calculator function that will be our main interface. The function should take three parameters: the two numbers that we want to operate on, as well as an operator:

    const calculator = function (first: number, second: number, op: Operator) {

    }

  4. Create a type alias for a function that does a calculation on two numbers. Such a function will take two numbers as parameters and return a single number:

    type Operation = (x: number, y: number) => number;

  5. Create an empty array that can hold multiple tuples of the [Operator, Operation] type. This will be our dictionary, where we store all our methods:

    const operations: [Operator, Operation][] = [];

  6. Create an add method that satisfies the Operation type (you don't need to explicitly reference it):

    const add = function (first: number, second: number) {

        return first + second;

    };

  7. Create a tuple of the Operator.Add value and the add function and add it to the operations array:

    operations.push([Operator.Add, add]);

  8. Repeat steps 6 and 7 for the subtraction, multiplication, and division functions:

    const subtract = function (first: number, second: number) {

        return first - second;

    };

    operations.push([Operator.Subtract, subtract]);

    const multiply = function (first: number, second: number) {

        return first * second;

    };

    operations.push([Operator.Multiply, multiply]);

    const divide = function (first: number, second: number) {

        return first / second;

    };

    operations.push([Operator.Divide, divide]);

  9. Implement the calculator function, using the operations array to find the correct tuple by the Operator provided, and then using the corresponding Operation value to do the calculation:

    const calculator = function (first: number, second: number, op: Operator) {

        const tuple = operations.find(tpl => tpl[0] === op);

        const operation = tuple[1];

        const result = operation(first, second);

        return result;

    }

    Note that, as long as a function has the required type, that is, it takes two numbers and outputs a number, we can use it as an operation.

  10. Let's take the calculator for a test run. Write some code that will call the calculator function with different arguments:

    console.log(calculator(4, 6, Operator.Add));

    console.log(calculator(13, 3, Operator.Subtract));

    console.log(calculator(2, 5, Operator.Multiply));

    console.log(calculator(70, 7, Operator.Divide));

  11. Save the file and run the following command in the folder:

    tsc calculator.ts

  12. Verify that the compilation ended successfully and that there is a calculator.js file generated in the same folder. Execute it in the node environment with the following command:

    node calculator.js

    You will see the output looks as follows:

    10

    10

    10

    10

  13. Now, let's try to extend our calculator by adding a modulo operation. First, we need to add that option to the Operator enum:

    enum Operator {

        Add = "add",

        Subtract = "subtract",

        Multiply = "multiply",

        Divide = "divide",

        Modulo = "modulo"

    }

  14. Add a function called modulo of the Operation type, and add a corresponding tuple to the operations array:

    const modulo = function (first: number, second: number) {

        return first % second;

    };

    operations.push([Operator.Modulo, modulo]);

  15. At the end of the file, add a call to the calculator function that uses the Modulo operator:

    console.log(calculator(14, 3, Operator.Modulo));

  16. Save and compile the file and run the resulting JavaScript with the following command:

    node calculator.js

    You will see an output that looks as follows:

    10

    10

    10

    10

    2

Note that when we extended our calculator with the modulo function, we did not change the calculator function at all. In this exercise, we saw how we can use the tuples, arrays, and function types to effectively design an extensible system.

Activity 1.01: Creating a Library for Working with Strings

Your task is to create a series of simple functions that will help you do some common operations on strings. Some of the operations are already supported in the standard JavaScript library, but you will use them as a convenient learning exercise, both of JavaScript internals and TypeScript as a language. Our library will have the following functions:

  1. toTitleCase: This will process a string and will capitalize the first letter of each word but will make all the other letters lowercase.

    Test cases for this function are as follows:

    "war AND peace" => "War And Peace"

    "Catcher in the Rye" => "Catcher In The Rye"

    "tO kILL A mOCKINGBIRD" => "To Kill A MockingBird"

  2. countWords: This will count the number of separate words within a string. Words are delimited by spaces, dashes (-), or underscores (_).

    Test cases for this function are as follows:

    "War and Peace" => 3

    "catcher-in-the-rye" => 4

    "for_whom the-bell-tolls" => 5

  3. toWords: This will return all the words that are within a string. Words are delimited by spaces, dashes (-), or underscores (_).

    Test cases for this function are as follows:

    "War and Peace" => [War, and, peace]

    "catcher-in-the-rye" => [catcher, in, the, rye]

    "for_whom the-bell-tolls"=> [for, whom, the, bell, tolls]

  4. repeat: This will take a string and a number and return that same string repeated that number of times.

    Test cases for this function are as follows:

    "War", 3 => "WarWarWar"

    "rye", 1 => "rye"

    "bell", 0 => ""

  5. isAlpha: This will return true if the string only has alpha characters (that is, letters). Test cases for this function are as follows:

    "War and Peace" => false

    "Atonement" => true

    "1Q84" => false

  6. isBlank: This will return true if the string is blank, that is, consists only of whitespace characters.

    Test cases for this function are as follows:

    "War and Peace" => false

    " " => true

    "" => true

    When writing the functions, make sure to think of the types of the parameters and the types of the return values.

    Note

    The code files for this activity can be found here: https://packt.link/TOZuy.

Here are some steps to help you create the preceding functions (note that there are multiple ways to implement each of the functions, so treat these steps as suggestions):

  1. Creating the toTitleCase function: In order to change each word, we'll need first to get all the words. You can use the split function to make a single string into an array of words. Next, we'll need to slice off the first letter from the rest of the word. We can use the toLowerCase and toUpperCase methods to make something lower- and uppercase, respectively. After we get all the words properly cased, we can use the join array method to make an array of strings into a single large string.
  2. Creating the countWords function: In order to get the words, we can split the original string on any occurrence of any of the three delimiters ( " ", "_", and "-"). Fortunately, the split function can take a regular expression as a parameter, which we can use to our benefit. Once we have the words in an array, we just need to count the elements.
  3. Creating the towards function: This method can use the same approach as the preceding one. Instead of counting the words, we'll just need to return them. Take note of the return type of this method.
  4. Creating the repeat function: Create an array with the required length (using the Array constructor), and set each element to the input value (using the array's fill method). After that, you can use the join method of the array to join the values into a single long string.
  5. Creating the isAlpha function: We can design a regular expression that will test this, but we can also split the string into single characters, using the string split method. Once we have the character array, we can use the map function to transform all the characters to lowercase. We can then use the filter method to return only those characters that are not between "a" and "z". If we don't have such characters, then the input only has letters, so we should return true. Otherwise, we should return false.
  6. Creating the isBlank function: One way to create such a function is to repeatedly test whether the first character is empty, and if it is, to remove it (a while loop works best for this). That loop will break either on the first non-blank characters or when it runs out of the first elements, that is, when the input is empty. In the first case, the string is not blank, so we should return false; otherwise, we should return true.

    Note

    The solution to this activity can be found via this link.

Summary

In this chapter, we looked at the world before TypeScript and described the problems and issues that TypeScript was actually designed to solve. We had a brief overview of how TypeScript operates under the hood, got ourselves introduced to the tsc compiler, and learned how we can control it using the tsconfig.json file.

We familiarized ourselves with the differences between TypeScript and JavaScript and saw how TypeScript infers the types from the values that we provide. We learned how different primitive types are treated in TypeScript, and finally, we learned how to create our own types to structure the building blocks of a large-scale, enterprise-level web application. Equipped with the fundamentals, you are now in a position to delve further into TypeScript, with the next chapter teaching you about declaration files.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset