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.
Over the next seven years, Microsoft evolved the language, adding additional features to make TypeScript more robust. Fast-forward to today and TypeScript (version 3.6.3 at the time of this writing) is one of the most popular languages for building (primarily) web-facing applications.
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.
But here’s where it gets interesting: what do you compile TypeScript to, exactly? The answer might surprise you:
TypeScript gets compiled to JavaScript!
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.
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 TypeScript code provided below 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 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!
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.
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, re-running 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?
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!
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.
Now, create a file named app.ts and in it put the code we just executed on the playground.
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).
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’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.
target (es5) – 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
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
In all cases, it’s just a colon, followed by one of the supported types, which we’ll look at now.
String
Now there are no games to be played: bestShowEver is a string, and that’s the end of it!
Number
Boolean
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
Either of those assignments will be okay with tsc because TypeScript will infer type any for the variable.
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
If you initialized an empty array, or had to initialization at all, then type any will be inferred, just like with variables.
TypeScript will complain about that because the pets array can only hold strings now.
You still use the :<type> syntax, but the array [] notation must be appended after the type.
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
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
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).
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
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
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).
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, and Undefined
That is okay, as would a null assignment to a variable of any other type.
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.
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!
Finally, 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.
Custom Type Aliases
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.
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.
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).
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”!
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
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
Here, we don’t need to type the function keyword, and we don’t even need to type the return keyword!
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
Try that with a plain old string, and you’ll face syntax errors, but with template literals, it works just fine.
Default Parameters
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)
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.
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?
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!
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.
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
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.
Member Visibility
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
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.
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.
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.
The get and set keywords prefixing a method indicate a getter and 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.
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:
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.
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 inBasePlanet 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!