In the previous four chapters, where we looked at Node and React, in both of those, you saw code written in JavaScript. That makes total sense given that Node uses Google’s V8 JavaScript engine to execute code, and React is (most usually at least) used to create browser-based applications, and browsers speak JavaScript (along with HTML and CSS of course).

But there is, at least arguably, a better option, one that overcomes many of the perceived shortcomings of JavaScript and makes for more robust code and easy maintenance of JavaScript-based applications. That option is called TypeScript, and in this chapter and the next, I’ll introduce to you the core concepts associated with what has become one of the hottest languages around.

As with Node and React, this chapter and the next are not an exhaustive discussion of the topic. You won’t learn every last nook and cranny TypeScript has to offer. But these chapters will build the foundation. Further concepts will be introduced in context in the coming chapters as necessary.

What Is TypeScript?

Somewhere around October of 2010, Microsoft started to realize that JavaScript, while becoming very popular, had several shortcomings that frequently lead to more error-prone code written by developers. In the eyes of some people, both inside and outside of Microsoft, JavaScript wasn’t a mature enough language for what was being built with it.

So, the company began an internal project to address what they, and many others, saw as problems with JavaScript. In October of 2010, they made public that project and called it TypeScript.

TypeScript can be thought of as a wrapper, of sorts, around JavaScript, or an extension to it. A key point to remember is that all valid JavaScript code is also valid TypeScript code. TypeScript, however, adds things on top of JavaScript, the key thing being data types, as the name clearly implies. This is a significant benefit because it allows IDEs and other developer tools to provide IntelliSense to the developer, that is, hints about what types are allowed in what situations. With JavaScript and its loosely typed nature, mistakes are easy to make, for example, passing a string where a numeric value is expected. With TypeScript, those sorts of errors are spotted quickly and easily by various tooling, and the key thing to remember is that these problems are caught at compile time, not at runtime as they are with JavaScript – and I dropped a pretty big spoiler there: unlike with JavaScript that just gets executed directly, there is a compilation step when dealing with TypeScript.

Over the next seven years, Microsoft evolved the language, adding additional features to make TypeScript more robust. Fast-forward to today and TypeScript (version 4.7.4 at the time of this writing) is one of the most popular languages for building (primarily) web-facing applications.

As I mentioned earlier, one key element that makes TypeScript different from JavaScript, however, is that you can’t run TypeScript code in a browser or Node, not natively at least. Neither browsers nor Node understands TypeScript; they only understand JavaScript. So, when working with TypeScript, there is a pre-execution step: you must compile TypeScript. One can imagine a day when, perhaps, browsers and Node will speak TypeScript natively, and no compilation will be necessary, but whether that day comes, it is not this day! So, for now, TypeScript must be compiled, or transpiled, as it is frequently termed, into JavaScript for execution.

When TypeScript is compiled to JavaScript, all the TypeScript-y bits are stripped out, leaving just plain old JavaScript. Data types, for example, are purely a development-time construct. Once compilation occurs, they are gone. But, because they were there during development, the compiler will flag type-related errors at that time, rather than having them be discovered at runtime, which is the case with JavaScript generally.

In addition to types, another big advantage of TypeScript is that it supports more modern JavaScript language features but can compile them down to older versions of JavaScript. This means that you can use newer features, such as arrow functions and async/await, even in browsers that don’t natively support them, because the compiler produces code that implements them for you in the older JavaScript dialect that the browser understands. In the last chapter, we talked about Babel, and in a real sense, the TypeScript compiler (which we’re going to discuss very soon) does the same thing Babel does in this regard (TypeScript can and does add features to the language as well, even some that will never appear in JavaScript itself).

Jumping into the Deep End

Now that you have a general idea of what TypeScript is, let’s get right down to business and see it in action! To do this, we’re going to first take advantage of a facility that you’ll find on TypeScript’s home page at typescriptlang.org called “the playground.” You’ll find it at typescriptlang.org/play, and on that playground, you can enter arbitrarily the following TypeScript code provided and execute it. It’s a great way to experiment with the language and a handy way for me as an author to give you your first look at TypeScript before we need to install any tooling for it!

In Figure 5-1, you can see what it should look like after you enter the example code.

Figure 5-1
A screenshot of typescript page. It has a dialog box, with the text, hello along with ok option, and codes in the background.

TypeScript in action, on the playground!

In this figure, I’ve executed the code, which you can see on the left. Now, there’s a couple of things to notice. First, see the red squiggly line underneath the humanName argument? If you hover over that, it will tell you that humanName implicitly has an any type. We’ll get to what this means shortly, but for now it’s enough to realize that the TypeScript playground is examining your code in real time and is pointing out that you haven’t specified a type for the argument (which may or may not be an error, which is why you can execute this code despite that being flagged). The critical point is that it does this before you run the code. That’s the point of types and TypeScript itself!

However, perhaps the bigger problem related to types is that you’ll notice that the alert() message doesn’t do what we expect, at least not based on what you’d logically conclude the point of the sayHi() function is. It’s intended to greet someone by name, but in this case, we’re passing an object to it. JavaScript doesn’t care of course, there’s nothing that tells it that humanName really should be a string that contains a person’s name, so it just produces an alert() message with the object passed in generically, which in this case doesn’t give us anything particularly useful and definitely not what we actually want it to produce.

With TypeScript, though, we can fix this! To do so, we add what’s called a type annotation to the argument by changing the sayHi() function’s definition to this:

function sayHi(humanName: string) {

The value after the colon is the type annotation, and this is the secret sauce you’ll see time and again in TypeScript code. With that change made, rerunning the example will result in the red squiggly going away from the humanName argument – because we’re now telling TypeScript what type we expect it to be – but now, we get a red squiggly underneath the entire object passed to sayHi(). If you hover over it, you will see the message “Argument of type ‘{ humanName: string }’ is not assignable to parameter of type ‘string’.” TypeScript is telling us, in no uncertain terms, that we can’t pass an object to a function that expects a string. Cool, right?

Of course, if we change the call to sayHi() to

sayHi("Luke Skywalker");

then the alert() shows us exactly what we expect: “Hello, Luke Skywalker!”

A few more things to note about this simple example is that TypeScript, based on the error message from the object, seems to have known that the value of the humanName property of the object passed to sayHi() was a string even though we didn’t explicitly tell it. This implicit typing is called type inference, and TypeScript does it any time you declare and initialize a variable in one go (which, obviously, works for object properties like here too). If you declare a variable without initializing it, though, or when a function argument doesn’t specify a type, then TypeScript assumes a specialized type: any. This effectively mimics how JavaScript works in that the variable or argument can take a value of any type at any time.

If you’re thinking that using TypeScript with nothing but type any references would be kind of silly, then you would be right! As a rule, in TypeScript, you should always declare your types. In fact, even when declaring and initializing in one statement, it’s still probably a good idea to declare the type. There may be situations where any makes sense, but you should explicitly decide that if so (and yes, you can declare a variable of argument as being of type any expressly).

Beyond the Playground

Now, the playground is a useful thing to have available, but clearly you aren’t going to be using it to develop your real applications. Instead, you’ll need to be able to compile and execute TypeScript code on your own machine, and that’s where your new best friend, tsc – the TypeScript compiler – comes in!

Installing it is simple, using our friend NPM:

npm install typescript

As usual, it’s your choice whether you want to install it locally or globally (remember that adding the -g argument installs an NPM package globally). Either way, this installs several TypeScript-related things, but the key thing that we care about here is tsc, the compiler, which is your one-stop shop for working with TypeScript code. It can act as a task runner and a bundler and can take the place of Babel, as previously mentioned.

Using it is simplicity itself: all you need to do is execute tsc and tell it what file to compile by doing

tsc <filename>

Note that you have to use npx to execute it instead:

npx tsc <filename>

Let’s go ahead and take it for a spin! Drop to a command line and navigate to an empty directory. Install TypeScript and then create a new file named index.html with the following contents:

<html>   <head></head>  <body>    <script src="app.js"></script>  </body> </html>

Now, create a file named app.ts and in it put the code we just executed on the playground.

Once that’s done, compile it using tsc with this command (or the npx equivalent):

tsc app.ts

After that, load up index.html in your favorite browser, and you should see the alert() message, just like on the playground.

If you look in the directory, you’ll see that an app.js file was created. That’s your compiled code, which is then loaded by index.html (notice that we do not load app.ts in index.html because the browser wouldn’t know what to do with it).

Looking at app.js, you can see what tsc has produced from your code:

function sayHi(humanName) {     alert("Hello, " + humanName + "!"); } sayHi("Luke Skywalker");

In this simple example, the only real difference is that the type information for humanName was stripped off, which confirms what I said earlier: types are only for development time, not runtime.

Configuring TypeScript Compilation

Now that you’ve seen how to install and use tsc, let’s talk a little bit more about it and about TypeScript projects.

Usually, your projects will be a bit more complex than just a single file to compile. Typically, you’ll have multiple .ts files to compile. You could pass all the names on the command line to tsc, but that will get burdensome in a hurry. Instead, you can create a file named tsconfig.json that will allow you to define your project a little bit and configure how tsc works.

You don’t need to write this file by hand, though! The tsc tool comes with a handy option, -init, that will create a basic tsconfig.json file for you. Go ahead and, in the same directory as the last section, run

tsc -init

You’ll find a tsconfig.json file has been created. The presence of that file effectively makes this directory the root of a TypeScript project as far as tsc goes.

One of the first benefits this provides is that now, you can execute tsc without any arguments, and it will dutifully compile any .ts files in the current directory, as well as subdirectories. That’s already worth the effort, right?

The tsconfig.json file isn’t required, as you saw earlier, and it has no required elements in it either. However, it does provide a large number of options to configure your project. I won’t be going over all of them here, but you can see all available options online at typescriptlang.org/docs/handbook/tsconfig-json.html.

I will, however, introduce options as needed throughout the remainder of these chapters, but for the most part, default options will be used. In fact, when you run that init comment, only the following options are enabled (with their default value shown in parentheses):

  • target (es2016) – Specifies the ECMAScript (JavaScript) target version that the generated JavaScript will adhere to: es3, es5, es2015, es2016, es2017, es2018, es2019, or esnext.

  • module (commonjs) – Specifies the module loader system that will be used (modules and loaders will be discussed in the next chapter): none, commonjs, amd, system, umd, es2015, or esnext.

  • strict (true) – Enables all strict type-checking options.

  • esModuleInterop (true) – Enables generation of interoperability code to allow for interoperability between CommonJS and ES modules via the creation of namespace objects for all imports.

  • forceConsistentCasingInFileNames – TypeScript follows the case sensitivity rules of the file system it’s running on. This can be problematic if some developers are working in a case-sensitive file system and others aren’t. If a file attempts to import fileManager.ts by specifying ./FileManager.ts, the file will be found in a case-insensitive file system, but not on a case-sensitive file system. When this option is set, TypeScript will issue an error if a program tries to include a file by a casing different from the casing on disk.

  • skipLibCheck – Skip type checking of declaration files. This can save time during compilation at the expense of type-system accuracy. For example, two libraries could define two copies of the same type in an inconsistent way. Rather than doing a full check of all d.ts files (which are files that define the types of libraries you might use in your own code), TypeScript will type-check the code you specifically refer to in your app’s source code. A common case where you might think to use skipLibCheck is when there are two copies of a library’s types in your node_modules. In these cases, you should consider using a feature like yarn’s resolutions to ensure there is only one copy of that dependency in your tree or investigate how to ensure there is only one copy by understanding the dependency resolution to fix the issue without additional tooling. Another possibility is when you are migrating between TypeScript releases and the changes cause breakages in node_modules and the JS standard libraries which you do not want to deal with during the TypeScript update.

As of this writing, these are the only options that are enabled by default, everything else is commented out (though helpfully is actually present, so you don’t have to go off looking everything up, most of it is right there for you if you need it). As I mentioned, by default, tsc will compile all files in the current directory and subdirectories (if necessary) if a tsconfig.json file is present. If you know you need it to skip specific files though, you can add the exclude element and then list the files not to compile. You can also explicitly include things with the files element. These are probably two of the most commonly used additional options, hence why I’m mentioning them here.

The Nitty-Gritty: Types

Now, let’s get to the main event in TypeScript and discuss the various types that it supports. As previously mentioned, types are declared with the :<type> syntax. This can be in a function declaration to type the arguments:

function sayHi(humanName: string) {

You can also declare the type a function returns:

function concatStrings(str1: string, str2: string): string { }

It can also be used in a variable declaration of course:

let a: string = "Hello";

In all cases, it’s just a colon, followed by one of the supported types, which we’ll look at now.

String

You’ve already seen the string type a few times, and it’s no different from the normal string in JavaScript:

const bestShowEver = "Babylon 5";

As mentioned earlier, TypeScript automatically infers the type of bestShowEver as string, which means this will result in a compiler error:

bestShowEver = 42;

But also, as I said before, it’s better to (nearly) always declare the type:

const bestShowEver: string = "Babylon 5";

Now there are no games to be played: bestShowEver is a string, and that’s the end of it!

Number

TypeScript offers a single number type. There are no integer vs. floating-point values in TypeScript; they’re all simply numbers. So you can do

const a: number = 42;

And you can also do

const b: number = 3.14;

And, of course, you’ll get an error if you try to do either of these:

a = "42"; b = "3.14";

As with JavaScript, TypeScript allows for hexadecimal and octal literals, though the way you specify octal is different than JavaScript:

const a: number = 0xf00d; const b: number = 0o744; // Zero followed by lower-case o

Boolean

The TypeScript boolean type is as simple as it gets; it has a value of true or false and nothing else:

const isThisTheBestBookEver: boolean = true;

Note that even though 0 and 1 are oftentimes interchangeable for boolean values and will be evaluated as boolean values in logic statements in at least some cases, TypeScript does not allow it:

const isThisTheBestBookEver: boolean = 1; // Compiler error

This extra rigidity helps avoid some tricky bugs, so let’s all say a hearty “thank you” to TypeScript for saving us from ourselves!

Any

The any type, which I mentioned earlier, is the type that a variable, argument, or function return will have if you don’t specify a type explicitly:

let accountBalance; accountBalance = 15000; accountBalance = "15000";

Either of those assignments will be okay with tsc because TypeScript will infer type any for the variable.

Of course, you can explicitly declare it to be any as well:

let food: any = "pizza"; food = 123; // This is now okay

Even if you’re using the any type (which you should consider carefully if you think you need to, because often you don’t really want to use it!), it’s still probably a good idea to always define it, so that anyone reading your code knows you intended it to be any.

Arrays

TypeScript supports arrays, of course; what good language doesn’t? Just like with variables, if you don’t specify a type, the type will be inferred from the initialization values:

const pets = [ "Belle", "Bubbles" ];

If you initialized an empty array, or had no initialization at all, then type any will be inferred, just like with variables.

This pets array becomes an array of strings, so you can’t later do

pets = [ 42 ];

TypeScript will complain about that because the pets array can only hold strings now.

You can also explicitly type an array with just subtly different syntax than for scalar variables:

const pets: string[] = [ "Belle", "Bubbles" ];

You still use the :<type> syntax, but the array [] notation must be appended after the type.

Now, what if you have an array where you really do want to allow for strings and numbers and perhaps other types? Well, this is a case where that any type may well be exactly what you want to use:

const pets: any[] = [ "Belle", 42 ];

But, again, consider this carefully! Is that really what you want to do, or might it be better to have two separate arrays? Only you can decide; I’m just suggesting that think it through either way.

Tuples

A tuple is just an array with a specific number of elements of specific types. In plain JavaScript, that might look like

const authors = [ "Frank", 46 ];

But, critically, there is no enforcement of anything in JavaScript. You could shove a number as the first element and even have a third element in the array. In TypeScript though, that’s all enforced, and the way it’s declared is with slightly different syntax:

const authors: [ string, number ] = [ "Frank", 46 ];

Here, we’re saying that the authors array must have two elements, and the first must be a string, and the second must be a number. As a result, this will not compile:

const authors: [ string, number ] = [ 46, "Frank" ];

The types of the elements are wrong here (they’re reversed). When you then access an element, say authors[1], the correct type will be returned, a string in that case. Since it’s a string, you could call substr() on it, for example, but trying to do the same on the element returned by authors[0] would result in an error since a number does not have that method available. Note too that accessing an element outside the set of known indices results in an error, so authors[2] will result in an error.

Enums

While JavaScript offers a string, a boolean, and a number type (despite it being a loosely typed language, the types do still technically exist), enums are something that JavaScript does not natively offer; they are purely a TypeScript construct. Enums serve to make specific sequences of numbers more human-readable and expressive. Take, for example, this plain JavaScript code:

const Pizza = 0; const FriedChicken = 1; const IceCream = 2;

It’s nice that we don’t need to remember that a value of 1 means fried chicken in our, I guess, horribly unhealthy food truck sales system – we can use the variable FriedChicken anywhere we need it – but it’s still kind of ugly that way to a lot of developers. With enums in TypeScript, which you declare with the new enum keyword, you can do it more elegantly:

enum Food { Pizza, FriedChicken, IceCream }; let myFavoriteFood: Food.FriedChicken; alert(myFavoriteFood);

That alert() call will show one because TypeScript begins assigning numbers to the named elements in the Food enum starting from zero. That’s the value Pizza gets. It then increments by one for each subsequent value, so FriedChicken gets assigned one, and IceCream gets assigned two (and so on, if we added more foods).

You can assign specific values if you wish too, either from the first element onward or anywhere in between. For example:

enum Food { Pizza, FriedChicken = 500, IceCream }; let myFavoriteFood: Food.FriedChicken; alert(myFavoriteFood);

Now, the value shown will be 500. But Food.Pizza would still have a value of zero since we didn’t assign it a specific value.

Now, here’s a good mystery for you: without trying it on the playground, what value will IceCream have? Does it have two, maybe, since TypeScript keeps numbering from where it left off? Or does it perhaps restart numbering, so assigns it zero?

No, it gets a value of 501. Anywhere you explicitly define a value, TypeScript will keep numbering subsequent items, by one, from that value on.

Function

TypeScript lets you declare a function type. This is a way of saying that a variable must reference a function with a particular signature. For example:

let myMathFunction: (num1: number, num2: number) => string;

Now, myMathFunction can only be assigned a value that is a function with two number arguments, and that returns a string. So, this is okay:

function add(n1: number, n2: number): string {   return "" + n1 + n2; } myMathFunction = add;

But this is not:

function multiply(a: number, b: number): number {   return a * b; } myMathFunction = multiply;

Even though the type being returned by multiply() seems to be correct – multiplying two numbers will yield a number – the contract that was defined for the myMathFunction variable says any function it references must return a string, whether that makes sense or not. Note that argument names don’t matter, only types do.

Object

Earlier, you saw an error message that seemed to indicate that TypeScript was performing type inference on object properties. As it happens, that’s precisely what it does:

let person = {   firstName : "John", lastName : "Sheridan", age : 52 };

Here, TypeScript infers the type of the object, including its properties, and this is termed an object type. That means that from this point on, person may only reference an object with three properties, firstName, lastName, and age, and they must have the types string, string, and number, respectively. Even trying to assign person = { } later will result in an error because TypeScript will see that as the property types not matching.

Similarly, trying to do person = { a :"John", b : "Sheridan", age : 52 } will be an error because, in contrast to function types, with object types the property names do matter (it’s only logical: object properties can be in any order, so there’s no way for TypeScript to reliably determine the types except by name).

Of course, you can also be more explicit if you wish:

let person: {   firstName: string, lastName: string, age: number } = {   firstName : "John", lastName : "Sheridan", age : 52 };

Note that the properties and values within the object definition do not have types defined. That would be redundant as they are already defined in the object type definition.

Null, Void, Undefined, and Never

Three other types are, conceptually, related to one another, so I’ve grouped them together here. Let’s start with null:

let favoriteCar = "Camaro"; favoriteCar = null;

Yes, TypeScript has a null type that is different from other “null-like” types. Interestingly, null is considered subtypes of all other types, which means you can assign them to anything:

let myFavoriteNumber: number = null; let myFavoriteString: string = null;

That is okay, as would a null assignment to a variable of any other type.

In a similar vein as null is undefined:

let favoriteCar;

Here, favoriteCar will have a value of undefined, which is different than null (a comparison of a variable with a value of null and another with a value of undefined will not evaluate as equal). But, like null, undefined is taken to be a subtype of every other type, so similarly you can do a literal assignment with it:

let favoriteCar = undefined;

But what if you do want to ensure a variable is never null? In that case, tsconfig.json is your friend! Under the compilerOptions section, add a key strictNullChecks and give it a value of true. With that done, the compiler will complain if you assign null or undefined to any variable except if it is declared as type any.

Note

If you want to have your mind blown, you could do let myFavoriteNumber: null = null;. This would mean you can only assign the value null to myFavoriteNumber. Similarly, let myFavoriteString: undefined = undefined; will only be allowed to have undefined assigned to it. It’s probably not useful in any way, but it’s a curious side effect of these two types. Finally, doing let favoriteCar = null; will result in favoriteCar having an inferred type of null, not any like you might expect, so effectively this variable can only ever be assigned a value of null!

The void type is conceptually like the opposite of any: it’s like having no type at all! The void type is typically only seen as the return type of a function, to indicate the function returns no value. While you declare a variable as type void, you can only ever assign a value of null to it (and then, only if strictNullChecks isn’t enabled). Note that TypeScript will figure out the return type by default, so most of the time, it isn’t necessary to specify void, though as long as you know that’s correct, then it’s probably better to be explicit.

Finally, never represents the type of values that never occur. For instance, never is the return type for a function expression or an arrow function expression that always throws an exception or one that never returns. Variables also acquire the type never when narrowed by any type guards that can never be true. The never type is a subtype of, and assignable to, every type; however, no type is a subtype of, or assignable to, never (except never itself). Even any isn’t assignable to never. For example:

function error(message: string): never {   throw new Error(message); } function infiniteLoop(): never {   while (true) {} } function fail() {   return error("Something failed"); }

All three of these functions have a return type of never (for that last one, it’s inferred). Never doesn’t get used all that often, but it’s worth being aware of.

Custom Type Aliases

Let’s say you want to create an object type to represent a person. You saw that earlier:

let person = {   firstName : "John", lastName : "Sheridan", age : 52 };

That’s all well and good, but what happens when you need to create two people? In that situation, you’re going to write something like this:

let person1 = {   firstName : "John", lastName : "Sheridan", age : 52 }; let person2 = {   firstName : "Michael", lastName : "Garibaldi", age : 53 };

As you can imagine, that’s not a great way to do things. As a rule, developers like to avoid duplicate code like that. Instead, TypeScript offers custom type aliases, which lets you provide a custom name for a type. To do so, you use the new type keyword, like so:

type PersonType = {   firstName: string, lastName: string, age: number };

From then on, you can use it like any other type:

let person1: PersonType = {   firstName : "John", lastName : "Sheridan", age : 52 }; let person2: PersonType = {   firstName : "Michael", lastName : "Garibaldi", age : 53 };

You can choose any name you like; it doesn’t have to have type in it as it does here. That way, if you want to change the type definition, you can do it in just one place.

You can even alias native types if you want:

type MyAwesomeString = string; let str: MyAwesomeString = "test";

There probably isn’t a whole lot of reason to do that, but you can (some people use it as a form of documentation in effect, but I personally would counsel against doing so).

Union Types

Sometimes, you’ll have a situation where you want a variable to be able to hold one of several different types, or you want an argument to accept one of various kinds, but you don’t want to use any. In that case, union types are the answer.

Take this code, for example:

let myAge: any; myAge = 46; myAge = "46";

It might be okay that myAge can store a number or a string because maybe the rest of your code can handle either. Maybe you’ve got some code like this:

if (typeof myAge == "string") {   alert(parseInt(myAge) * 2); } else if (typeof myVar == "number") {   alert(myAge * 2); }

But what if you tried to do this?

myAge = true;

Unfortunately, that will be allowed because myAge is of type any, and being able to assign a boolean to it would be bad since our code doesn’t handle that (plus, for a variable that, presumably, stores a person’s age, a boolean doesn’t make sense).

So, instead, we can use a union type, which is denoted with the pipe character:

let myAge: number | string; myAge = 46; myAge = "46"; myAge = true;

You can read that as saying that myAge can be of type number or type string. Therefore, the first two assignment statements will be okay, but the third, trying to assign a boolean, will result in a compiler error.

TypeScript == ES6 Features for “Free”!

Recall that when you compile a TypeScript file with tsc, it’s really doing a transpilation. It’s “compiling” from TypeScript to JavaScript. Earlier, I said that tsc does much the same thing as Babel does, and that’s true in this regard. The implication of this is that TypeScript supports most ES6 features. It doesn’t support all of them, though, so it’s good to know which you should avoid. Fortunately, there is a handy chart you can use here: kangax.github.io/compat-table/es6.

Let’s not be negative though, let’s talk about some of the features you can use! Note that the assumption is that you already have some JavaScript knowledge, so I’m not going to cover every last thing in intricate detail, but certainly, these are probably the most important things that you should be aware of.

The let and const Keywords

First, as all the example code I’ve shown so far do, you can freely use the let and const keywords (let for variables you want to be able to change the value of later, const for those you don’t). Yes, you can still use the var keyword too, but it’s suggested you don’t since both let and const have block scope rather than var’s global scope, which helps avoid a lot of insidious bugs.

Block Scope

Speaking of block scope, that’s another important one! In JavaScript, variables for a long time could only be declared with var. Such variables have function scope (or global scope when declared outside of any function). That means that you can do some “weird” things like this:

function test() {     if (true) {       var greeting = "hello";     }     alert(greeting); } test();

Most people see it as a bit weird that you can alert(greeting) there and have it work despite greeting being declared inside the if block. Well, with let, that problem is solved! That same code, by just changing var to let, results in greeting only being available inside the block it’s declared in, the if statement in this case. If you enter that on the TypeScript playground, it will even flag it as an error. This helps avoid some interesting problems that can crop up when using var. The same is true for const, but with that you get the addition of not being able to change the variable’s value later. As a general rule, you should use const whenever possible, or let when it’s not, and avoid var unless you have a specific reason to use it.

Arrow Functions

With all the examples shown so far, I’ve used the standard function definition format that uses the function keyword. If, like the Dude from The Big Lebowski, you’re into the whole brevity thing though, you can use arrow functions instead:

const test = (name) => {   alert(`Hello, ${name}`); } test("Jack");

Arrow functions allow you to skip typing the function keyword all the time. They can be even shorter if the function returns a value:

const addNums = (a, b) => a + b; alert(addNums(2, 3));

Here, we don’t need to type the function keyword, and we don’t even need to type the return keyword!

But we’re talking about TypeScript, and yet there are no types here; this is just plain JavaScript. Not to worry, you can type things as well with arrow functions:

const addNums = (a: number, b: number): number => a + b; alert(addNums(2, 3));

But brevity isn’t the only benefit of arrow functions and maybe not even the biggest. That distinction probably goes to how the keyword this is handled. In plain JavaScript, what this points to can vary depending on how functions are called. With arrow functions, though, lexical scope is used, which means that whatever contains the function is what this will point to at execution time. If the function is in global scope, then this will point to the window object (assuming we’re executing in a browser). If the function is inside an object, then this will point to that object. It’s simple and consistent, and TypeScript makes it available to you!

Template Literals

Something worth noting in that last example is another ES6 feature that you can use in TypeScript: template literals. The backtick ` character denotes a template literal string, and within it you can insert any valid JavaScript (or TypeScript) expression by wrapping it in ${}. Note that I said expression there because while you can insert variables as shown, you can do arbitrarily complex things:

alert(`Hello, ${name.toUpperCase().substr(2)}`);

Another great thing about template literals is that they can span multiple lines of source code:

alert(`Hello,   your name is   ${name} `);

Try that with a plain old string, and you’ll face syntax errors, but with template literals, it works just fine.

Default Parameters

With TypeScript, you also can use default parameter values. That means that you can do this:

const multNums = (a: number, b: number = 10): number => a * b; alert(multNums(3));

Here, we’re saying that if the second number isn’t supplied when multNums() is called, then it should have the default value 10. Hence, we get 30 in the alert() when this is executed. This is a simple thing that winds up saving you a lot of time and extra code, so it’s truly nice to have in TypeScript too.

Spread and Rest (and As an Added Bonus: Optional Arguments)

The spread operator, which is three periods together, allows an iterable item, things like arrays or strings, to be expanded in places where zero or more arguments (in the case of function calls) or elements (for array literals) are expected or an object expression to be expanded in places where zero or more key-value pairs (for object literals) are expected. As an example:

const addNums = (a: number, b: number): number => a + b; const nums: number[] = [ 5, 6 ]; alert(addNums(...nums));

The idea here is that we want to “spread” the values in the nums array into the arguments passed to addNums. This is in contrast to writing something like

alert(addNums(nums[0], nums[1]));

However, if you try that code, you’ll find that there is an error on the ...nums spread argument passed to addNums() that says “Expected 2 arguments, but got 0 or more.” The issue here is that because there’s a variable number of possible arguments, TypeScript can’t make a proper determination about what to do. Fortunately, there are at least two ways to fix this. First, you could do this:

const addNums = (a?: number, b?: number): number => a + b; const nums: number[] = [ 5, 6 ]; alert(addNums(...nums));

See those question marks after the arguments of the addNums() function? Those are how you indicate optional arguments in TypeScript. Doing that tells TypeScript that the argument may or may not be present. In this case, we’re saying that both can be optional. Now, that doesn’t make much sense from the perspective of what the function does, but it does result in TypeScript not flagging this as an error. Note that when using optional arguments, they must always come last, meaning you can’t do (a?: number, b: number) because a required argument can’t come after an optional one.

However, note that if you pass in three or more values in the nums array, only the first two are added. The function works the same as it did before; the optional arguments only serve to get around the syntax error situation.

The other way you could fix this is by the use of the rest operator, which is simply the spread operator in a different place:

const addNums = (...a: number[]): number =>   a.reduce((acc, val) => acc + val); const nums: number[] = [ 5, 6 ]; alert(addNums(...nums));

Any argument prefixed with the operator means that zero or more arguments can be in that place. The result is that you’ll get an array inside the function, named as the argument is named, that contains all passed in values in that place. As with optional arguments, rest arguments must come last. With this approach, given we have an array, we can use the reduce() method to add up all the numbers passed in. In contrast to the optional argument approach, this solution results in all the numbers being added, no matter how many are passed in, so it is functionally different, and, therefore, which way you go depends on what you’re trying to do.

Destructuring

TypeScript supports two forms of destructuring: object and array. And while knowing that is great, knowing what destructuring is would be even better, no?

Consider the following object:

const person = {   firstName : "Billy", lastName : "Joel", age : 70 };

Now, if you want to grab the values out of that object, you might do

const firstName = person.firstName; const lastName = person.lastName; const age = person.age;

That’ll work, but it’s an awful lot of typing! With destructuring, you can get at that data more concisely:

const { firstName, lastName, age } = person;

Now, you’ll have three separate variables named firstName, lastName, and age, and their values will be taken from the person object, because TypeScript (really JavaScript) knows, by virtue of you using the curly braces around the variables, the names of the properties in the object you want to pull out and does so for you. Sweet!

Arrays can be destructured in the same way:

const vals = [ "Billy", "Joel", 70 ]; const [ firstName, lastName, age ] = vals; alert(firstName); alert(lastName); alert(age);

Here, of course, it’s based on order: TypeScript is essentially just doing firstName=vals[0] and lastName=vals[1] and age=vals[2] for you under the covers.

And, as a bonus, for your next job interview, if you get asked the question of how to swap the value of two variables without using a third, here’s an answer using array destructuring in TypeScript:

let x = 1; let y = 2; [ x, y ] = [ y, x ]; alert(x); // 2 alert(y); // 1

Here, the array being destructured is created on the fly on the right-hand side of the equals, and then it’s just array destructuring as described in the preceding text. “You’re welcome” in advance for when you ace that interview and make a ton of money at your new job!

Classes

The final topic I want to cover in this chapter is classes. JavaScript, at least of the ECMAScript 5 and higher variety, supports classes, but TypeScript alters the syntax a bit and adds a fair bit of – wait for it – class! It really classes the place up is what I’m saying! (I know, I know, terrible dad joke!)

Properties

First, when it comes to properties, rather than having to declare them in a constructor, you can do them a little more elegantly. Instead of this

class Planet {   constructor() {     this.name = null;     this.mass = null;   } }

you can instead do this:

class Planet {   name: string;   mass: number; }

It may not be a huge difference, but it makes JavaScript look a lot more like other object-oriented languages syntactically.

As you would expect, you can declare your types for the properties like anywhere else in TypeScript as you see there, but now, you don’t need to embed them in a constructor. You, of course, still can have a constructor, and that looks the same as in plain JavaScript, but your property declarations are external to the constructor now, if you supply one at all.

Naturally, if you want to set property values at construction time, then you can still do that in the constructor:

class Planet {   name: string;   mass: number;   constructor(inName: string, inMass: number) {     this.name = inName;     this.mass = inMass;   } }

Member Visibility

TypeScript adds the notion of member visibility to classes. With plain JavaScript classes, all members are public, that is, available to all other code (there are some tricks you can play to simulate private members so that they are only available to the code of the class itself, but it’s not something offered by the language intrinsically). As with plain JavaScript, TypeScript’s default visibility is public, but now you can have both private and protected members:

class Planet {   private name: string = "none";   protected mass: number;   constructor(inName: string, inMass: number) {     this.name = inName;     this.mass = inMass;   }   public printName() {     alert(this.name);   } }

Here, the name property will only be accessible by code within this class. The mass property will be accessible by code within this class as well as by code in any class that extends this one. Putting public before the printName() method is optional since that’s the default, but you definitely can do so if you want to be explicit. Note too that, as I’ve done for name, you can assign a value as part of the declaration if you wish.

Inheritance

When I described protected, I said that it allows the member to be accessible to code within the class and to code in classes that extend it. That provides for inheritance, which is another capability that TypeScript adds (or, more precisely, augments). Let’s create a specific planet:

class Jupiter extends Planet {   private colorBands: boolean = true;   constructor() {     super("Jupiter", 1234);   } }

Now we can do

let j: Jupiter = new Jupiter();

Now we’ve got an object of type Jupiter, which extends from the Planet class. A few things of note here. First, members can be added to the subclass, as with the colorBands property. Second, calling j.printName() works as expected, because printName() has public visibility. But, if you try to do alert(j.name), then you’ll find that you get an error from TypeScript saying that “Property ‘name’ is private and only accessible within class ‘Planet’.” The same is true if you try to alert(j.mass).

However, understand that while j.printName() will print Jupiter’s name, if you try to put a method in the Jupiter class itself that accesses name, that won’t work. Private members are not inherited, so while the code of the Planet class knows about name and can work with it, the code in the Jupiter class does not and so can’t do anything with it (you could, of course, call methods in the base class from the child class to work with it though).

Another thing of note is that if a subclass has a constructor, as Jupiter does, then it must call the superclass’s constructor via the super() reference.

An interesting point to understand is that you can override anything in the superclass in the child class, as you can in any good object-oriented language. You must be aware, though, that for properties, those defined in the body of the child class will override any value passed into the constructor. So, if you add protected mass: number = 5555; to the Jupiter class, then the value of its mass property will be 5555 no matter what you pass into the constructor.

With TypeScript, you can override members in the parent class as in most other object-oriented languages, and that works as you’d expect. However, TypeScript doesn’t support method overloading in the way most people expect. For example, this won’t work in the Planet class:

public calcSuperMass(): number {   return this.mass * 2; } public calcSuperMass(): string {   return "" + this.mass * 2; }

The compiler will complain that you have a duplicate function implementation even though the return types are different. Even a different argument list isn’t enough:

public calcSuperMass(): number {   return this.mass * 2; } public calcSuperMass(a: number): string {   return "" + this.mass * a; }

That still won’t be allowed for the same reason. Now, all hope is not lost, though: you can, in effect, achieve overloading by using optional parameters or default parameters. So, you could do either of the following:

public calcSuperMass(massMultiple?: number): number {   if (massMultiple) {     return this.mass * massMultiple;   }   return this.mass * 2; } // Or: public calcSuperMass(massMultiple: number = 2): number {   return this.mass * massMultiple; }

In the first approach, since massMultiple is marked optional, you can effectively have calcSuperMass() work whether you pass in an argument or not, at the cost of the branching inside the function. In the second approach, you can skip that logic because now a has a default value even if you don’t pass it.

I would suggest the second is the better way, but either will achieve the goal. Now, if you instead want to overload the type of an argument, you could use a union type:

public calcSuperMass(a: number | string): number {   if (typeof a === "number") {     return this.mass * a;   } else {     return this.mass * parseInt(a);   } }

That, too, will work. However, it’s probably worse than either of the other two since you are in a sense (kinda/sorta/maybe) going around the type system. But, while it probably doesn’t make much sense in this instance, you certainly could have situations where you do want a single function to handle multiple types, in which case this approach gives you a way to do overloading like you want.

Getters and Setters

It is generally considered an excellent pattern to make data members in classes private and then, when necessary, provide outside access to them via getter methods (or accessor methods). Similarly, allowing private members to be set through setter (or mutator) methods is also typically considered good form. Especially for setters, this enables you to have some code that checks incoming values to ensure they are valid in whatever way makes sense for your application.

Because this is so common a pattern, TypeScript offers syntax specifically for these types of methods:

class Planet {   private _name: string = "No name set";   get name() {     return `This planet's name is '${this._name}'.`;   }   set name(inName: string) {     if (inName === "Pluto") {       this._name = "Not a planet";     } else {       this._name = inName;     }   } } let p: Planet = new Planet(); alert(p.name); // 'No name set'. p.name = "Pluto"; alert(p.name); // 'Not a planet' (sorry, little guy!) p.name = "Venus"; alert(p.name); // 'Venus'

The get and set keywords prefixing a method indicate a getter and a setter method, respectively. What this does for you is it allows you to access the member by the name of the method. In other words, p.name is the same as executing p.name() would be, but you can’t call p.name() because TypeScript will tell you that “This expression is not callable. Type ‘String’ has no call signatures.”, which is just a fancy way of saying that getters and setters aren’t methods in the usual sense, but they do execute when you access or set the property that matches the method’s name. Note too the use of the _name identifier for the actual property name. TypeScript doesn’t require the underscore – you can use any name you wish – but the key point is that the getter and setter method names cannot be the same as that of the private property they access, and prefixing with an underscore is a popular choice to ensure they aren’t the same but are still related in some logical way.

Also, worth noting is that you can make read-only properties by only supplying a getter. Another way to achieve this is by prefixing the property with the readonly keyword:

class Planet {   readonly name: string = "No name set"; } let p: Planet = new Planet(); alert(p.name); // Okay p.name = "Neptune"; // Error

Remember, name will be public by default, which means you normally can do p.name = "Neptune". But, with readonly before it like that, tsc will give an error on that line. This is a good choice if you have properties that you do want accessible, but you know can never be changed by outside code since it will save you from having to provide even a getter. As with much of newer JavaScript and TypeScript, saving a little typing appears to be the primary goal sometimes!

Static Members

TypeScript classes also provide for static members, both properties and methods:

class Planet {   static theBorgLiveHere: boolean = true; } alert(Planet.theBorgLiveHere); // true

Notice that, in contrast to all the properties and methods you’ve seen so far that are tied to instances of a class and are thus called instance members, we can access the value of theBorgLiveHere without an instance of Planet being created first. That’s the very definition of static, and it’s just that easy with TypeScript!

Abstract Classes

The final topic related to classes to discuss is abstract classes. An abstract class is simply one that cannot itself be instantiated. It is always meant to be a base class that others extend from. They serve a similar function as interfaces, a topic we’ll look at in the next chapter, but the primary difference is that an abstract class can provide some amount of implementation for methods while an interface cannot.

So, by way of example:

abstract class BasePlanet {   name: string;   radius: number;   constructor(inName: string, inRadius: number) {     this.name = inName;     this.radius = inRadius;   }   abstract collapseToBlackHole(inMoreMass: number): void;   calcDiameter() {     return this.radius * 2;   } }

Given this class, with the abstract keyword before the class keyword indicating this is an abstract class, we can never have an instance of BasePlanet. Instead, we can only have, perhaps, Earth instances:

class Earth extends BasePlanet {   collapseToBlackHole(inAdditionalMass: number) {     // Perform physics-breaking 2001-like monolith magic here   } }

The other thing to note is that while BasePlanet implements calcDiameter() – because calculating the diameter of a planet from its radius is the same for all planets (well, basically the same; this is a book about programming after all, not astrophysics, so we can ignore some intricacies I think!) – it does not implement collapseToBlackHole(). The declaration of that method in BasePlanet is declared abstract, just like that class, which is a thing you can totally do! And besides, it has no function body, so it wouldn’t do much even if that were syntactically allowed. That means that an extending class must implement it, as the Earth class does (and, again, since Stephen Hawking is not here to correct us – rest in peace, good sir – we’ll ignore the fact that collapsing any body to a black hole essentially comes down to enough mass in a small enough diameter, so you probably wouldn’t need each child class to implement that method either). This is an excellent way to “push” the common functionality into a base class while still ensuring that an extending class implements those things that it really must implement to be a valid instance of the base class in a logical sense.

Summary

In this chapter, you got your first look at TypeScript. You got some historical perspective and then saw some of the biggest things it adds to JavaScript, including types (obviously!), ES6 features like arrow functions, template literals, and classes. You learned how to compile TypeScript to JavaScript and a little about how to configure that compilation.

In the next chapter, we’ll look at more of what TypeScript brings to the table, including concepts like namespaces, modules, interfaces, decorators, and a bit about debugging. Once through this chapter and the next, you’ll have the foundational knowledge about TypeScript on which we can begin to build some projects, along with Node, React, and a few other tools. But let’s not put the cart before the horse. Jump over to the next chapter to continue on with TypeScript!