Learn React with TypeScript 3

4.4 (5 reviews total)
By Carl Rippon
  • Instant online access to over 7,500+ books and videos
  • Constantly updated with 100+ new titles each month
  • Breadth and depth in over 1,000+ technologies
  1. TypeScript Basics

About this book

React today is one of the most preferred choices for frontend development. Using React with TypeScript enhances development experience and offers a powerful combination to develop high performing web apps. In this book, you’ll learn how to create well structured and reusable react components that are easy to read and maintain by leveraging modern web development techniques.

We will start with learning core TypeScript programming concepts before moving on to building reusable React components. You'll learn how to ensure all your components are type-safe by leveraging TypeScript's capabilities, including the latest on Project references, Tuples in rest parameters, and much more. You'll then be introduced to core features of React such as React Router, managing state with Redux and applying logic in lifecycle methods. Further on, you'll discover the latest features of React such as hooks and suspense which will enable you to create powerful function-based components. You'll get to grips with GraphQL web API using Apollo client to make your app more interactive. Finally, you'll learn how to write robust unit tests for React components using Jest.

By the end of the book, you'll be well versed with all you need to develop fully featured web apps with React and TypeScript.

Publication date:
November 2018
Publisher
Packt
Pages
502
ISBN
9781789610253

 

Chapter 1. TypeScript Basics

Facebook has become an incredibly popular app. As its popularity grew, so did the demand for new features. React was Facebook's answer to help more people work on the codebase and deliver features quicker. React worked so well for Facebook that they eventually open sourced it. Today, React is a mature library for building component-based frontends that is extremely popular and has a massive community and ecosystem.

TypeScript is also a popular, mature library maintained by a big company – namely, Microsoft. It allows users to add strong types to their JavaScript code, helping them to be more productive, particularly in large code bases.

This book will teach you how you can use both of these awesome libraries to build robust frontends that are easy to maintain. The first couple of chapters in the book focus solely on TypeScript. You'll then start to learn about React and how you can compose robust frontends using Typescript components with strong typing.

In this chapter, we'll coverTypeScript's relationship to JavaScript and the benefits it brings. A basic understanding of JavaScript is therefore required. We'll also cover the basics of TypeScript that you'll commonly use whenwriting code for the browser.

You'll come to understand the need to use TypeScript for building a frontend and the sort of projects for which TypeScript really shines. You will also see how to transpile your TypeScript code into JavaScript so that it can run in a browser. Last but not least, you'll learn how you can perform additional checks on your TypeScript code to make it readable and maintainable.

By the end of the chapter, you'll be ready to start learning how you can use TypeScript for building frontends with React.

In this chapter, we'll cover the following topics:

  • Understanding the benefits of TypeScript
  • Understanding basic types
  • Creating interfaces, types aliases, and classes
  • Structuring code into modules
  • Configuring compilation
  • TypeScript linting
  • Code formatting
 

Technical requirements


We will use the following technologies in this chapter:

  • TypeScript playground: This is a website at https://www.typescriptlang.org/play/ that allows you to play around with and understand the features in TypeScript without installing it.
  • Node.js andnpm: TypeScript and React are dependent on these. You can install them at: https://nodejs.org/en/download/. If you already have these installed, make sure Node.js is at least Version 8.2 andnpm is at least Version 5.2.
  • TypeScript: This can be installed via npm, entering the following command in a terminal:
npm install -g typescript

Note

All the code snippets in this chapter can be found online at: https://github.com/carlrip/LearnReact17WithTypeScript/tree/master/01-TypeScriptBasics

 

Understanding the benefits of TypeScript


When a JavaScript codebase grows, it can become hard to read and maintain. TypeScript is an extension of JavaScript, adding static types. The TypeScript compiler reads in TypeScript code that includes type information and produces clean, readable JavaScript with the type information transformed and removed. The compiled code can then run in our favorite browsers and Node.js.

TypeScript offers several benefits over JavaScript:

  • Coding errors can be caught in the development process earlier
  • Static types allow tools to be built that improve the developer experience and productivity
  • JavaScript features that aren't implemented in all the browsers yet can actually be used in an app that targets those browsers

We'll go through these points in detail in the following sections.

Catching coding errors early

The type information helps the TypeScript compiler catch bugs and typos before our users run into them. In code editors such as Visual Studio Code, a mistake is underlined in red immediately after the user has gone wrong. As an example, create a file called utils.js and paste in the following code, which calculates the total price on an order line:

function calculateTotalPrice(product, quantity, discount) {
  var priceWithoutDiscount = product.price * quantity;
  var discountAmount = priceWithoutDiscount * discount;
  return priceWithoutDiscount - discountAmount;
}

There is a bug in the code that might be difficult for us to spot. If we open the file in Visual Studio Code, no errors are highlighted. If we change the extension of the file to .ts, Visual Studio Code immediately underlines bits of the code that need our attention in red:

Most of the errors are TypeScript asking for some type information. So, let's add some types to our code:

interface IProduct {
  name: string;
  unitPrice: number;
}

function calculateTotalPrice(product: IProduct, quantity: number, discount: number): number {
  var priceWithoutDiscount: number = product.price * quantity;
  var discountAmount: number = priceWithoutDiscount * discount;
  return priceWithoutDiscount - discountAmount;
}

Don't worry if you don't understand what we just added; we'll go through types in the next section. The key point is that we now have a single error highlighted to us, which is, in fact, the bug:

The bug is that our function references a price property in the product object that doesn't exist. The property that we should reference is unitPrice.

Better developer experience and productivity

Let's fix the bug in the previous section by renaming price to unitPrice. Notice how Visual Studio Code gives us IntelliSense lists unitPrice as an option because it looking at our type definition:

Here, TypeScript and Visual Studio Code are using the types to provide a better authoring experience for us. As well as IntelliSense, we are provided with code navigation features, and the safe renaming of functions and variables across multiple files. These features increase our productivity, particularly when the code base is large and there is a team of people working on it.

Using future JavaScript features

There is another benefit of TypeScript that is important to understand. TypeScript allows us to use some features in JavaScript that haven't yet been adopted by all browsers but still target those browsers. TypeScript achieves this by transpiling the use of these features down to JavaScript code that the targeted browser does support.

As an example, let's look at the exponentiation operator (**) in ES7, which isn't supported in IE. Let's create a file calledfuture.ts and enter the following code:

var threeSquared: number = 3 ** 2;
console.log(threeSquared);

When we run the program in a browser, it should put 9 into the console. Before we do that, let's run the code against the TypeScript compiler to get the transpiled JavaScript. Run the following command in a terminal in the same directory as future.ts:

tsc future

This should generate a file called future.js with the following content:

var threeSquared = Math.pow(3, 2);
console.log(threeSquared);

So, TypeScript converted the exponentiation operator to a call to the Math.pow function, which is supported in IE. To confirm that this works, paste the generated JavaScript code into the console in IE and the output should be 9.

This example is purposely simple but probably not that useful. Async/await, spread operators, rest parameters, and arrow functions are far more useful features that IE doesn't support but TypeScript allows the use of. Don't worry if you don't know what the features in the last sentence are, as we'll cover them when we need them in the book.

 

Understanding basic types


We touched on types in the last section. In this section, we'll go through the basic types that are commonly used in TypeScript so that we start to understand what cases we should use in each type. We'll make heavy use of the online TypeScript playground, so be sure to have that ready.

Primitive types

Before understanding how we declare variables and functions with types in TypeScript, let's briefly look at primitive types, which are the most basic types. Primitive types are simple values that have no properties. TypeScript shares the following primitive types with JavaScript:

  • string: Represents a sequence of Unicode characters
  • number: Represents both integers and floating-point numbers
  • boolean: Represents a logical true or false
  • undefined: Represents a value that hasn't been initialized yet
  • null: Represents no value

Type annotations

Types for JavaScript variables are determined at runtime. Types for JavaScript variables can also change at runtime. For example, a variable that holds a number can later be replaced by a string. Usually, this is unwanted behavior and can result in a bug in our app.

TypeScript annotations let us declare variables with specific types when we are writing our code. This allows the TypeScript compiler to check that the code adheres to these types before the code executes at runtime. In short, type annotations allow TypeScript to catch bugs where our code is using the wrong type much earlier than we would if we were writing our code in JavaScript.

TypeScript annotations let us declare variables with typesusing the :Type syntax.

  1. Let's browse to the TypeScript playground and enter the following variable declaration into the left-hand pane:
let unitPrice: number;
  1. The transpiled JavaScript will appear on the right-hand side as follows:
var unitPrice;

Note

That let has been converted to var. This is because the compiler that the playground uses is set to target a wide range of browsers, some of which don't supportlet. Also, notice that the type annotation has disappeared. This is because type annotations don't exist in JavaScript.

  1. Let's add a second line to our program:
unitPrice = "Table";

Notice that a red line appears under unitPrice, and if you hover over it, you are correctly informed that there is a type error:

  1. You can also add type annotations to function parameters for the return value using the same :Typesyntax. Let's enter the following function into the playground:
function getTotal(unitPrice: number, quantity: number, discount: number): number {
  const priceWithoutDiscount = unitPrice * quantity;
  const discountAmount = priceWithoutDiscount * discount;
  return priceWithoutDiscount - discountAmount;
}

We've declared unitPrice, quantity, and discount parameters, all as numbers. The return type annotation comes after the function's parentheses, which is also a number in the preceding example.

Note

We have used both const and let to declare variables in different examples. letwill allow the variable to change the value after the declaration, whereas const variables can't change. In the preceding function, priceWithoutDiscount and discountAmount never change the value after the initial assignment, so we have used const.

  1. Let's call our function with an incorrect type for quantity and assign the result to a variable with an incorrect type:
let total: string = getTotal(500, "one", 0.1);

We find that one is underlined in red, highlighting that there is a type error:

  1. If we then correct one to 1, total should be underlined in red, highlighting that there is a type problem with that:

The TypeScript compiler uses type annotations to check whether values assigned to variables and function parameters are valid for their type.

This strong type checking is something that we don't get in JavaScript, and it is very useful in large code bases because it helps usimmediatelydetect type errors.

Type inference

We have seen how type annotations are really valuable, but they involve a lot of extra typing. Luckily, TypeScript's powerful type inference system means we don't have to provide annotations all the time. We can use type inference when we immediately set a variable value.

Let's look at an example:

  1. Let's add the following variable assignment in the TypeScript playground:
let flag = false;
  1. If we hover our mouse over the flag variable, we can see that TypeScript has inferred the type as boolean:
  1. If we add another line beneath this, to incorrectly set flag to Table, we get a type error:

So, when we declare a variable and immediately set its type, we can use type inference to save a few keystrokes.

Any

What if we declare a variable with no type annotation and no value? What does TypeScript infer as the type? Let's enter the following code in the TypeScript playground and find out:

let flag;

If we hover our mouse over flag, we see it has been given the any type:

So, the TypeScript compiler gives a variable with no type annotation and no immediately assigned value, the any type. The any type is specific to TypeScript; it doesn't exist in JavaScript. It is a way of opting out of type checking on a particular variable. It is commonly used for dynamic content or values from third-party libraries. However, TypeScript's increasingly powerful type system means that we need to use any less often these days.

Void

void is another type that doesn't exist in JavaScript. It is generally used to represent a non-returning function.

Let's look at an example:

  1. Let's enter the following function into the TypeScript playground:
function logText(text: string): void {
  console.log(text);
}

The function simply logs some text into the console and doesn't return anything. So, we've marked the return type as void.

  1. If we remove the return type annotation and hover over the function name, logText, we'll see that TypeScript has inferred the type to be void:

This saves us a few keystrokes while writing functions that don't return anything.

Never

The never type represents something that would never occur and is typically used to specify unreachable areas of code. Again, this doesn't exist in JavaScript.

Time for an example:

  1. Type the following code into the TypeScript playground:
function foreverTask(taskName: string): never {
  while (true) {
    console.log(`Doing ${taskName} over and over again ...`);
  }
}

The function invokes an infinite loop and never returns, and so we have given it a type annotation of never. This is different to void because void means it will return, but with no value.

Note

In the preceding example, we used a JavaScript template literal to construct the string to log to the console. Template literals are enclosed by back-ticks (``) and can include a JavaScript expression in curly braces prefixed with a dollar sign (${expression}). Template literals are great when we need to merge static text with variables.

  1. Let's change the foreverTask function to break out of the loop:
function foreverTask(taskName: string): never {
  while (true) {
    console.log(`Doing ${taskName} over and over again ...`);
    break;
  }
}

The TypeScript compiler quite rightly complains:

  1. Let's now remove the break statement and the never type annotation. If we hover over the foreverTask function name with our mouse, we see that TypeScript has inferred the type to be void, which is not what we want in this example:

The never type is useful in places where the code never returns. However, we will probably need to explicitly define the never type annotation because the TypeScript compiler isn't smart enough yet to infer that.

Enumerations

Enumerations allow us to declare a meaningful set of friendly names that a variable can be set to. We use the enumkeyword, followed by the name we want to give to it, followed by the possible values in curly braces.

Here's an example:

  1. Let's declare an enum for order statuses in the TypeScript playground:
enum OrderStatus {
  Paid, 
  Shipped, 
  Completed, 
  Cancelled 
}
  1. If we look at the transpiled JavaScript, we see that it looks very different:
var OrderStatus;
(function (OrderStatus) {
    OrderStatus[OrderStatus["Paid"] = 1] = "Paid";
    OrderStatus[OrderStatus["Shipped"] = 2] = "Shipped";
    OrderStatus[OrderStatus["Completed"] = 3] = "Completed";
    OrderStatus[OrderStatus["Cancelled"] = 4] = "Cancelled";
})(OrderStatus || (OrderStatus = {}));

This is because enumerations don't exist in JavaScript, so the TypeScript compiler is transpiling the code into something that does exist.

  1. Let's declare a status variable, setting the value to the shipped status:
let status = OrderStatus.Shipped; 

Notice how we get nice IntelliSense when typing the value:

  1. By default, the numerical values start from 0 and increment. However, the starting value can be explicitly declared in the enum, as in the following example, where we set Paid to 1:
enum OrderStatus {
  Paid = 1, 
  Shipped, 
  Completed, 
  Cancelled 
}
  1. Let's set our status variable to the shipped status and log this to the console:
let status = OrderStatus.Shipped;
console.log(status);

If we run the program, we should see 2 output in the console:

  1. In addition, all the values can be explicitly declared, as in the following example:
enum OrderStatus {
  Paid = 1, 
  Shipped = 2, 
  Completed = 3, 
  Cancelled = 0
}

Enumerations are great for data such as a status that is stored as a specific set of integers but actually has some business meaning. They make our code more readable and less prone to error.

Objects

The object type is shared with JavaScript and represents a non-primitive type. Objects can contain typed properties to hold bits of information.

Let's work through an example:

  1. Let's enter the following code into the TypeScript playground, which creates an object with several properties of information:
const customer = {
  name: "Lamps Ltd", 
  turnover: 2000134, 
  active: true 
};

If we hover over name, turnover, and active, we'll see that TypeScript has smartly inferred the types to be string, number, and boolean respectively.

  1. If we hover over the customer variable name, we see something interesting:
  1. Rather than the type being object, it is a specific type with name, turnover, and active properties. On the next line, let's set the turnover property to some other value:
customer.turnover = 500000;

As we type the turnover property, IntelliSense provides the properties that are available on the object:

Note

We used const to declare the customer variable and then was able to change one of its property values later in the program. Shouldn't this have thrown an error? Well, the customer variable reference hasn't changed — just some properties within it. So, this is fine with the TypeScript compiler.

  1. This line of code is perfectly fine, so we don't get any complaints from the compiler. If we set the turnover to a value that has an incorrect type, we'll be warned as we would expect:
  1. Now let's set a property on customer that doesn't exist yet:
customer.profit = 10000;

We'll see that TypeScript complains:

This makes sense if we think about it. We've declared customer with name, turnover, and active properties, so setting a profit property should cause an error. If we wanted a profit property, we should have declared it in the original declaration.

In summary, the object type is flexible because we get to define any properties we require, but TypeScript will narrow down the type to prevent us incorrectly typing a property name.

Arrays

Arrays are structures that TypeScript inherits from JavaScript. We add type annotations to arrays as usual, but with square brackets at the end to denote that this is an array type.

Let's take a look at an example:

  1. Let's declare the following array of numbers in the TypeScript playground:
const numbers: number[] = [];

Here, we have initialized the array as empty.

  1. We can add an item to the array by using the array'spush function. Let's add the number 1 to our array:
numbers.push(1);

Note

We used const to declare the numbers variable and was able to change its array elements later in the program. The array reference hasn't changed – just the elements within it. So, this is fine with the TypeScript compiler.

  1. If we add an element with an incorrect type, the TypeScript compiler will complain, as we would expect:
  1. We can use type inference to save a few keystrokes if we declare an array with some initial values. As an example, if we type in the following declaration and hover over the numbers variable, we'll see the type has been inferred as number[].
const numbers = [1, 3, 5];
  1. We can access an element in an array by using the element number in square brackets. Element numbers start at 0.

Let's take an example:

  1. Let's log out the number of elements under the numbers variable declaration, as follows:
console.log(numbers[0]); 
console.log(numbers[1]); 
console.log(numbers[2]); 
  1. Let's now click the Run option on the right-hand side of the TypeScript playground to run our program. A new browser tab should open with a blank page. If we press F12 to open the Developer tools and go to the console section, we'll see 1, 3, and 5 output to the console.
  1. There are several ways to iterate through elements in an array. One option is to use a for loop, as follows:
for (let i in numbers) {
  console.log(numbers[i]); 
}

If we run the program, we'll see1, 3, and 5output to the console again.

  1. Arrays also have a useful function for iterating through their elements, called forEach. We can use this function as follows:
numbers.forEach(function (num) {
  console.log(num);
});
  1. forEach calls a nested function for each array element, passing in the array element. If we hover over the num variable, we'll see it has been correctly inferred as a number. We could have put a type annotation here, but we have saved ourselves a few keystrokes:

Arrays are one of the most common types we'll use to structure our data. In the preceding examples, we've only used an array with elements having a number type, but any type can be used for elements, including objects, which in turn have their own properties.

 

Creating interfaces, types aliases, and classes


In theUnderstanding basic typessection, we introduced ourselves to objects, which are types that can have their own properties. Interfaces, type aliases, and classes are ways that we can define an object structure before we start using it.

Following here is the customer object we worked with, where we declared the customer variable with an initial object value:

const customer = {
  name: "Lamps Ltd", 
  turnover: 2000134, 
  active: true 
};
  1. Let's try to declare the customer variable and set its value on a subsequent line:
let customer: object;
customer = {
  name: "Lamps Ltd", 
  turnover: 2000134, 
  active: true 
};
  1. So far, so good. However, let's see what happens when we try to change the customers turnover value:
customer.turnover = 2000200;
  1. The lack of IntelliSense when we type turnover isn't what we are used to. When we've finished typing the line, we get a compiler error:

The TypeScript compiler doesn't know about the properties in the customer object and so thinks there's a problem.

So, we need another way of defining an object structure with the ability to set property values later in the program. That's where interfaces, type aliases, and classes come in; they let us define the structure of an object by letting us define our own types.

Interfaces

An interface is a contract that defines a type with a collection of property and method definitions without any implementation. Interfaces don't exist in JavaScript, so they are purely used by the TypeScript compiler to enforce the contract by type checking.

We create an interface with theinterfacekeyword, followed by its name, followed by the bits that make up the interface in curly braces:

interface Product {
  ...
}

Properties

Properties are one of the elements that can be part of an interface. Properties can hold values associated with an object. So, when we define a property in an interface, we are saying that objects that implement the interface must have the property we have defined.

Let's start to play with an interface in the TypeScript playground:

  1. Enter the following interface:
interface Product {
  name: string;
  unitPrice: number;
}
  1. The preceding example creates a Product interface with name and unitPrice properties. Let's go on to use this interface by using it as the type for a table variable:
const table: Product = {
  name: "Table",
  unitPrice: 500
}
  1. Let's try to set a property that doesn't exist in the interface:
const chair: Product = {
  productName: "Table",
  price: 70
}

As expected, we get a type error:

  1. Properties on an interface can reference another interface because an interface is just a type. The following example shows an OrderDetail interface making use of a Product interface:
interface Product {
  name: string;
  unitPrice: number;
}

interface OrderDetail {
  product: Product; 
  quantity: number;
}

const table: Product = {
  name: "Table",
  unitPrice: 500
}

const tableOrder: OrderDetail = {
  product: table,
  quantity: 1
};

This gives us the flexibility to create complex object structures, which is critical when writing large, complex apps.

Method signatures

Interfaces can contain method signatures as well. These won't contain the implementation of the method; they define the contracts for when interfaces are used in an implementation.

Let's look at an example:

  1. Let's add a method to the OrderDetail interface we just created. Our method is called getTotal and it has a discount parameter of type number and returns a number:
interface OrderDetail {
  product: Product; 
  quantity: number;
getTotal(discount: number): number; 
}

Notice that the getTotal method on the interface doesn't specify anything about how the total is calculated – it just specifies the method signature that should be used.

  1. Having adjusted our OrderDetail interface, our tableOrder object, which implemented this interface, will now be giving a compilation error. So, let's resolve the error by implementing getTotal:
const tableOrder: OrderDetail = {
  product: table,
  quantity: 1,
getTotal(discount: number): number {
    const priceWithoutDiscount = this.product.unitPrice *  
    this.quantity;
    const discountAmount = priceWithoutDiscount * discount;
    return priceWithoutDiscount - discountAmount;
     }
};

Notice that the implemented method has the same signature as in the OrderDetail interface.

Note

The method implementation uses the this keyword to get access to properties on the object. If we simply referenced product.unitPrice and quantity without this, we would get a compilation error, because TypeScript would assume these variables are local within the method.

  1. Let's tweak the method signature to discover what we can and can't do. We'll start by changing the parameter name:
getTotal(discountPercentage: number): number {
  const priceWithoutDiscount = this.product.unitPrice *   
   this.quantity;
  const discountAmount = priceWithoutDiscount * 
  discountPercentage;
  return priceWithoutDiscount - discountAmount;
}
  1. We'll see that we don't get a compilation error. Let's change the method name now:
total(discountPercentage: number): number {
  const priceWithoutDiscount = this.product.unitPrice * this.quantity;
  const discountAmount = priceWithoutDiscount * discountPercentage;
  return priceWithoutDiscount - discountAmount;
} 
  1. This does cause an error because a total method doesn't exist on the OrderDetail interface:
  1. We could try changing the return type:
const tableOrder: OrderDetail = {
  product: table,
  quantity: 1,
  getTotal(discountPercentage: number): string {
    const priceWithoutDiscount = this.product.unitPrice * this.quantity;
    const discountAmount = priceWithoutDiscount * discountPercentage;
    return (priceWithoutDiscount - discountAmount).toString();
  }
};

This actually doesn't produce a compilation error in the TypeScript playground, but it should do!

  1. So, let's use Visual Studio Code for this example. After we've opened Visual Studio Code in a folder of our choice, let's create a file called interfaces.ts and paste in the interface definitions for the Product and OrderDetail interfaces, along with the table variable declaration.
  2. We can then enter the preceding implementation of the OrderDetail interface. As expected, we get a compilation error:
  1. Changing the parameter type also results in a compilation error:

The errors provided by TypeScript are fantastic—they are very specific about where the problem is, allowing us to quickly correct our mistakes.

  1. So, when implementing a method from an interface, the parameter names aren't important, but the other parts of the signature are.In fact, we don't even need to declare the parameter names in the interface:
interface OrderDetail {
  ...
  getTotal(number): number;
}

However, omitting the parameter names arguably makes the interface harder to understand—how do we know exactly what the parameter is for?

Optional properties and parameters

We might want to make a property optional because not every situation where the interface is implemented requires it. Let's take the following steps in our OrderDetail interface:

  1. Let's create an optional property for the date it was added. We specify an optional value by putting a?at the end of the property name but before the type annotation:
interface OrderDetail {
  product: Product;
  quantity: number;
  dateAdded?: Date,
  getTotal(discount: number): number;
}

We'll see that our implementation of this interface, tableOrder, isn't broken. We can choose to add dateAdded to tableOrder but it isn't required.

  1. We might also want to make a method parameter optional. We do this in a similar way by putting a?after the parameter name. In our example, let's make discount optional in the OrderDetail interface:
interface OrderDetail {
  product: Product;
  quantity: number;
  dateAdded?: Date,
  getTotal(discount?: number): number;
}
  1. We can change the method implementation signature as well:
getTotal(discount?: number): number {
  const priceWithoutDiscount = this.product.unitPrice * this.quantity;
  const discountAmount = priceWithoutDiscount * (discount || 0);
  return priceWithoutDiscount - discountAmount;
}

We've also dealt with the case when a discount isn't passed into the method by using (discount || 0) in the discountAmount variable assignment.

Note

x || y is shorthand for if x is truthy then use x, otherwise, use y. The following values are falsy values:false, 0, "", null, undefined, and NaN. All other values are truthy.

  1. With our optional parameter in place, we can call getTotal without passing a value for the discount parameter:
tableOrder.getTotal()

The preceding line doesn't upset the TypeScript compiler.

Readonly properties

We can stop a property from being changed after it has initially been set by using the readonly keyword before the property name.

  1. Let's give this a try on our Product interface by making the name property readonly:
interface Product {
readonly name: string;
  unitPrice: number;
}
  1. Let's also make sure we have an instance of the Product interface in place:
const table: Product = {
  name: "Table",
  unitPrice: 500
};
  1. Let's change the name property table now on the next line:
table.name = "Better Table";

As expected, we get a compilation error:

readonly properties are a simple way of freezing their values after being initially set. A common use case is when you want to code in a functional way and prevent unexpected mutations to a property.

Extending interfaces

Interfaces can extend other interfaces so that they inherit all the properties and methods from its parent. We do this using the extends keyword after the new interface name and before the interface name that is being extended.

Let's look at the following example:

  1. We create a new interface, taking Product as a base, and add information about discount codes:
interface Product {
  name: string;
  unitPrice: number;
}

interface DiscountCode {
  code: string;
  percentage: number;
}

interface ProductWithDiscountCodes extends Product {
  discountCodes: DiscountCode[];
}
  1. We can create an instance of the interface in the usual way, filling in properties from the base interface as well as the child interface:
const table: ProductWithDiscountCodes = {
  name: "Table",
  unitPrice: 500,
  discountCodes: [
    { code: "SUMMER10", percentage: 0.1 },
    { code: "BFRI", percentage: 0.2 }
  ]
};

Interfaces allow us to create complex but flexible structured types for our TypeScript program to use. They are a really important feature that we can use to create a robust, strongly-typed TypeScript program.

Type aliases

In simple terms, a type alias creates a new name for a type. To define a type alias, we use the type keyword, followed by the alias name, followed by the type that we want to alias.

We'll explore this with the following example:

  1. Let's create a type alias for the getTotal method in the OrderDetail interface we have been working with. Let's try this in the TypeScript playground:
type GetTotal = (discount: number) => number;

interface OrderDetail {
  product: Product;
  quantity: number;
  getTotal: GetTotal;
}

Nothing changes with objects that implement this interface – it is purely a way we can structure our code. It arguably makes the code a little more readable.

  1. Type aliases can also define the shape of an object. We could use a type alias for our Product and OrderDetail types that we previously defined with an interface:
type Product = {
  name: string;
  unitPrice: number;
};

type OrderDetail = {
  product: Product;
  quantity: number;
  getTotal: (discount: number) => number;
};
  1. We use these types in exactly the same way as we used our interface-based types:
const table: Product = {
  name: "Table",
  unitPrice: 500
};

const orderDetail: OrderDetail = {
  product: table,
  quantity: 1,
  getTotal(discount: number): number {
    const priceWithoutDiscount = this.product.unitPrice * this.quantity;
    const discountAmount = priceWithoutDiscount * discount;
    return priceWithoutDiscount - discountAmount;
  } 
};

So, type aliases seem very similar to interfaces. What is the difference between a type alias and an interface? The main difference is that type aliases can't be extended or implemented from like you can with interfaces. So, for a simple structure that doesn't require inheritance, should we use an interface or should we use a type alias? There isn't strong reasoning to prefer either approach. However, we should be consistent with whichever approach we choose to improve the readability of our code.

Classes

Classes feature in many programming languages, including JavaScript. They let us shape objects with type annotations in a similar way to interfaces and type aliases. However, classes have many more features than interfaces and type aliases, which we'll explore in the following sections.

Basic classes

Classes have lots of features. So, in this section we'll look at the basic features of a class. We use the class keyword followed by the class name, followed by the definition of the class.

Let's look at this in more depth with the following example:

  1. We could use a class to define the Product type we previously defined as an interface and as a type alias:
class Product {
  name: string;
  unitPrice: number;
}
  1. We create an instance of our Product class by using the new keyword followed by the class name and parentheses. We then go on to interact with the class, setting property values or calling methods:
const table = new Product();
table.name = "Table"; 
table.unitPrice = 500;

Notice that when we use this approach we don't need a type annotation for the table variable because the type can be inferred.

Classes have many more features than type aliases and interfaces though. One of these features is the ability to define the implementation of methods in a class.

Let's explore this with an example:

  1. Let's change the OrderDetail type we have been working within previous sections to a class. We can define the implementation of the getTotal method in this class:
class OrderDetail {
  product: Product;
  quantity: number;

  getTotal(discount: number): number {
    const priceWithoutDiscount = this.product.unitPrice * this.quantity;
    const discountAmount = priceWithoutDiscount * discount;
    return priceWithoutDiscount - discountAmount;
  }
}
  1. We can create an instance of OrderDetail, specifying a product and quantity, and then calling the getTotal method with a discount to get the total price:
const table = new Product();
table.name = "Table"; 
table.unitPrice = 500; 

const orderDetail = new OrderDetail();
orderDetail.product = table;
orderDetail.quantity = 2;

const total = orderDetail.getTotal(0.1);

console.log(total);

If we run this and look at the console, we should see an output of 900.

Implementing interfaces

We can use classes and interfaces together by defining the contract in an interface and then implementing the class as per the interface. We specify that a class is implementing a particular interface using the implements keyword.

As an example, we can define an interface for the order detail and then a class that implements this interface:

interface IOrderDetail {
  product: Product;
  quantity: number;
  getTotal(discount: number): number;
}

class OrderDetail implements IOrderDetail {
  product: Product;
  quantity: number;

  getTotal(discount: number): number {
    const priceWithoutDiscount = this.product.unitPrice *  
     this.quantity;
    const discountAmount = priceWithoutDiscount * discount;
    return priceWithoutDiscount - discountAmount;
  }
}

In the preceding example, we've prefixed the interface with I so that readers of the code can quickly see when we are referencing interfaces.

Why would we use this approach? It seems like more code than we need to write. So, what's the benefit? This approach allows us to have multiple implementations of an interface, which can be useful in certain situations.

Constructors

Constructors are functions that perform the initialization of new instances of a class. In order to implement a constructor, we implement a function called constructor. It's common to set property values in the constructor to simplify consumption of the class.

Let's look at the following example:

  1. Let's create a constructor in the OrderDetail class that allows us to set the product and quantity:
class OrderDetail implements IOrderDetail {
  product: Product;
  quantity: number;

constructor(product: Product, quantity: number) {
    this.product = product;
    this.quantity = quantity;
  }

  getTotal(discount: number): number {
    ...
  }
}
  1. If we create an instance of the class, we are forced to pass in the product and quantity:
const orderDetail = new OrderDetail(table, 2);
  1. This is nice because we've reduced three lines of code to one line. However, we can make our class even nicer to work with by making the default quantity parameter 1 if nothing is passed in:
constructor(product: Product, quantity: number = 1) {
  this.product = product;
  this.quantity = quantity;
}
  1. We now don't have to pass in a quantity if it is 1:
const orderDetail = new OrderDetail(table);
  1. We can save ourselves a few keystrokes and let the TypeScript compiler implement the product and quantity properties by using the public keyword before the parameters in the constructor:
class OrderDetail implements IOrderDetail {
  constructor(public product: Product, public quantity: number = 1) {
    this.product = product;
    this.quantity = quantity;
  }

  getTotal(discount: number): number {
    ...
  }
}

Extending classes

Classes can extend other classes. This is the same concept as interfaces extending other interfaces, which we covered in theExtending interfacessection. This is a way for class properties and methods to be shared with child classes.

As with interfaces, we use the extends keyword followed by the class we are extending. Let's look at an example:

  1. Let's create a ProductWithDiscountCodes from our Product class:
class Product {
  name: string;
  unitPrice: number;
}

interface DiscountCode {
  code: string;
  percentage: number;
}

class ProductWithDiscountCodes extends Product {
  discountCodes: DiscountCode[];
}
  1. We can then consume the ProductWithDiscountCodes class as follows, leveraging properties from the base class as well as the child class:
const table = new ProductWithDiscountCodes();
table.name = "Table";
table.unitPrice = 500;
table.discountCodes = [
  { code: "SUMMER10", percentage: 0.1 },
  { code: "BFRI", percentage: 0.2 }
];
  1. If the parent class has a constructor, then the child class will need to pass the constructor parameters using a function called super:
class Product {
  constructor(public name: string, public unitPrice: number) {
  }
}

interface DiscountCode {
  code: string;
  percentage: number;
}

class ProductWithDiscountCodes extends Product {
  constructor(public name: string, public unitPrice: number) {
super(name, unitPrice);
  }
  discountCodes: DiscountCode[];
}

Abstract classes

Abstract classes are a special type of class that can only be inherited from and not instantiated. They are declared with the abstract keyword, as in the following example:

  1. We can define a base Product class as follows:
abstract class Product {
  name: string;
  unitPrice: number;
}
  1. If we try to create an instance of this, the compiler will complain, as we would expect:
  1. We can create a more specific usable class for food products by extending Product:
class Food extends Product {
  constructor(public bestBefore: Date) {
    super();
  }
}
  1. Here, we are adding a bestBefore date in our Food class. We can then create an instance of Food, passing in the bestBefore date:
const bread = new Food(new Date(2019, 6, 1));

Abstract classes can have abstract methods that child classes must implement. Abstract methods are declared with the abstract keyword in front of them, as in the following example:

  1. Let's add an abstract method to our base Product class:
abstract class Product {
  name: string;
  unitPrice: number;
abstract delete(): void;
}
  1. After we add the abstract method, the compiler immediately complains about our Food class because it doesn't implement the delete method:
  1. So, let's fix this and implement the delete method:
class Food extends Product {
deleted: boolean;

  constructor(public bestBefore: Date) {
    super();
  }

delete() {
    this.deleted = false;
  }
}

Access modifiers

So far, all our class properties and methods have automatically had the public access modifier. This means they are available to interact with class instances and child classes. We can explicitly set the public keyword on our class properties and methods immediately before the property or method name:

class OrderDetail {
public product: Product;
public quantity: number;

public getTotal(discount: number): number {
    const priceWithoutDiscount = this.product.unitPrice * this.quantity;
    const discountAmount = priceWithoutDiscount * discount;
    return priceWithoutDiscount - discountAmount;
  }
}

As you might have guessed, there is another access modifier, called private, which allows the member to only be available to interact with inside the class and not on class instances or child classes.

Let's look at an example:

  1. Let's add a delete method in our OrderDetail class, which sets a private deleted property:
class OrderDetail {
  public product: Product;
  public quantity: number;
private deleted: boolean;

public delete(): void {
    this.deleted = true;
  }
  ...
}
  1. Let's create an instance of OrderDetail and try to access the deleted property:
const orderDetail = new OrderDetail();
orderDetail.deleted = true;

As expected, the compiler complains:

There is a third access modifier, protected, which allows the member to be available to interact with inside the class and on child classes, but not on class instances.

Property setters and getters

Our classes so far have had simple property declarations. However, for more complex scenarios, we can implement a property with a getter and a setter. When implementing getters and setters, generally, you'll need a private property to hold the property value:

  • getter is a function with the property name and the get keyword at the beginning and no parameters. Generally, this will return the value of the associated private property.
  • setter is a function with the same name with the set keyword at the beginning and a single parameter for the value. This will set the value of the associated private property.
  • The private property is commonly named the same as the getter and setter with an underscore in front.

Let's take a look at an example:

  1. Let's create getters and setters for the unitPrice property in our Product class. The setter ensures the value is not less than 0. The getter ensures null or undefined is never returned:
class Product {
  name: string;

  private _unitPrice: number;
  get unitPrice(): number {
    return this._unitPrice || 0;
  }
  set unitPrice(value: number) {
    if (value < 0) {
      value = 0;
    }
    this._unitPrice = value;
  }
}
  1. Let's consume the Product class and try this out:
const table = new Product();
table.name = "Table";
console.log(table.unitPrice); 
table.unitPrice = -10;
console.log(table.unitPrice); 

If we run this, we should see two 0's in the console.

Static

Static properties and methods are held in the class itself and not in class instances. They can be declared using the static keyword before the property or method name.

Let's look at the following example:

  1. Let's make the getTotal method static on the OrderDetail class we have been using:
class OrderDetail {
  product: Product;
  quantity: number;

static getTotal(discount: number): number {
    const priceWithoutDiscount = this.product.unitPrice * this.quantity;
    const discountAmount = priceWithoutDiscount * discount;
    return priceWithoutDiscount - discountAmount;
  }
}
  1. We get compilation errors where we try to reference the properties on the class. This is because the static method isn't in the class instance and therefore can't access these properties:
  1. To make the static method work, we can move its dependencies on the class instance to parameters in the function:
static getTotal(unitPrice: number, quantity: number, discount: number): number {
  const priceWithoutDiscount = unitPrice * quantity;
  const discountAmount = priceWithoutDiscount * discount;
  return priceWithoutDiscount - discountAmount;
}
  1. We can now call the static method on the class type itself, passing in all the parameter values:
const total = OrderDetail.getTotal(500, 2, 0.1);
console.log(total);

If we run the preceding program, we should get an output of 900in the console.

 

Structuring code into modules


By default, TypeScript generated JavaScript code that executes in what is called the global scope. This means code from one file is automatically available in another file. This in turn means that the functions we implement can overwrite functions in other files if the names are the same, which can cause our applications to break.

Let's look at an example in Visual Studio Code:

  1. Let's create a file called product.ts and enter the following interface for a product:
interface Product {
  name: string;
  unitPrice: number;
}
  1. Let's create another file, called orderDetail.ts, with the following content:
class OrderDetail {
  product: Product;
  quantity: number;
  getTotal(discount: number): number {
    const priceWithoutDiscount = this.product.unitPrice * this.quantity;
    const discountAmount = priceWithoutDiscount * discount;
    return priceWithoutDiscount - discountAmount;
  }
}

The compiler doesn't give us any complaints. In particular, the reference to the Product interface in the OrderDetail class is able to be resolved, even though it's in a different file. This is because both Product and OrderDetail are in the global scope.

Operating in the global scope is problematic because item names can conflict across different files, and as our code base grows, this is harder to avoid. Modules resolve this issue and help us write well organized and reusable code.

Module formats

Modules feature in JavaScript as part of ES6, which is great. However, lots of code exists in other popular module formats that came before this standardization. TypeScript allows us to write our code using ES6 modules, which can then transpile into another module format if specified.

Here is a brief description of the different module formats that TypeScript can transpile to:

  • Asynchronous Module Definition (AMD): This is commonly used in code targeted for the browser and uses a define function to define modules.
  • CommonJS: This format is used in Node.js programs. It uses module.exports to define modules and require to define dependencies.
  • Universal Module Definition (UMD): This can be used in both browser apps and Node.js programs.
  • ES6: This is the native JavaScript module format and uses the export keyword to define modules and import to define dependencies.

In the following sections (and, in fact, this whole book), we'll write our code using ES6 modules.

Exporting

Exporting code from a module allows it to be used by other modules. In order to export from a module, we use the export keyword. We can specify that an item is exported using export directly before its definition. Exports can be applied to interfaces, type aliases, classes, functions, constants, and so on.

Let's start to adjust our example code from the previous section to operate in modules rather than the global scope:

  1. Firstly, let's export the Product interface:
export interface Product {
  name: string;
  unitPrice: number;
}
  1. After we make this change, the compiler will complain about the reference to the Product interface in the OrderDetail class:

This is because Product is no longer in the global scope but OrderDetail still is. We'll resolve this in the next section, but let's look at alternative ways we can export the Product interface first.

  1. We can use an export statement beneath the item declarations. We use the export keyword followed by a comma-delimited list of item names to export in curly braces:
interface Product {
  name: string;
  unitPrice: number;
}

export { Product }
  1. With this approach, we can also rename exported items using the as keyword:
interface Product {
  name: string;
  unitPrice: number;
}

export { Product as Stock }

Importing

Importing allows us to import items from an exported module. We do this using an import statement that includes the item names to import in curly braces and the file path to get the items from (excluding the ts extension). We can only import items that are exported in the other module file.

  1. Let's resolve the issue with our OrderDetail class by importing the Product interface:
import { Product } from "./product";

class OrderDetail {
  product: Product;
  quantity: number;
  getTotal(discount: number): number {
    const priceWithoutDiscount = this.product.unitPrice * this.quantity;
    const discountAmount = priceWithoutDiscount * discount;
    return priceWithoutDiscount - discountAmount;
  }
}
  1. We can rename imported items using the as keyword in an import statement. We then reference the item in our code using the new name:
import { Product as Stock } from "./product";

class OrderDetail {
  product: Stock;
  quantity: number;
  getTotal(discount: number): number {
    const priceWithoutDiscount = this.product.unitPrice * this.quantity;
    const discountAmount = priceWithoutDiscount * discount;
    return priceWithoutDiscount - discountAmount;
  }
}

Default exports

We can specify a single item that can be exported by default using the default keyword:

export default interface {
  name: string;
  unitPrice: number;
}

Notice that we don't need to name the interface. We can then import a default exported item using an import statement without the curly braces with a name of our choice:

import Product from "./product";
 

Configuring compilation


We need to compile our TypeScript code before it can be executed in a browser. We do this by running the TypeScript compiler, tsc, on the files we want to compile. TypeScript is very popular and is used in many different situations:

  • It is often introduced into large existing JavaScript code bases
  • It comes by default in an Angular project
  • It is often used to add strong types to a React project
  • It can even be used in Node.js projects

All these situations involve slightly different requirements for the TypeScript compiler. So, thecompiler gives us lots of different options to hopefully meet the requirements of our particular situation.

  1. Let's give this a try by opening Visual Studio Code in a new folder and creating a new file, called orderDetail.ts, with the following content:
export interface Product {
  name: string;
  unitPrice: number;
}

export class OrderDetail {
  product: Product;
  quantity: number;
  getTotal(discount: number): number {
    const priceWithoutDiscount = this.product.unitPrice * this.quantity;
    const discountAmount = priceWithoutDiscount * discount;
    return priceWithoutDiscount - discountAmount;
  }
}
  1. We can open a Terminal in Visual Studio Code by going to the View menu and choosing Terminal. Let's enter the following command in the Terminal:
tsc orderDetail
  1. Hopefully, no errors should be output from the compiler and it should generate a file called orderDetail.js, containing the following transpiled JavaScript:
"use strict";
exports.__esModule = true;
var OrderDetail = (function () {
    function OrderDetail() {
    }
    OrderDetail.prototype.getTotal = function (discount) {
        var priceWithoutDiscount = this.product.unitPrice * this.quantity;
        var discountAmount = priceWithoutDiscount * discount;
        return priceWithoutDiscount - discountAmount;
    };
    return OrderDetail;
}());
exports.OrderDetail = OrderDetail;

We'll continue to use orderDetail.ts in the following sections as we explore how the compiler can be configured.

Common options

As mentioned earlier, there are lots of configuration options for the TypeScript compiler. All the configuration options can be found at https://www.typescriptlang.org/docs/handbook/compiler-options.html. The following sections detail some of the more common options that are used.

--target

This determines the ECMAScript version the transpiled code will be generated in.

The default is ES3, which will ensure the code works in a wide range of browsers and their different versions. However, this compilation target will generate the most amount of code because the compiler will generate polyfill code for features that aren't supported in ES3.

The ESNext option is the other extreme, which compiles to the latest supported proposed ES features. This will generate the least amount of code, but will only work on browsers that have implemented the features we have used.

As an example, let's compile orderDetail.ts targeting ES6 browsers. Enter the following in the terminal:

tsc orderDetail--target es6

Our transpiled JavaScript will be very different from the last compilation and much closer to our source TypeScript because classes are supported in es6:

export class OrderDetail {
    getTotal(discount) {
        const priceWithoutDiscount = this.product.unitPrice * this.quantity;
        const discountAmount = priceWithoutDiscount * discount;
        return priceWithoutDiscount - discountAmount;
    }
}

--outDir

By default, the transpiled JavaScript files are created in the same directory as the TypeScript files. --outDir can be used to place these files in a different directory.

Let's give this a try and output the transpiled orderDetail.js to a folder called dist. Let's enter the following in the terminal:

tsc orderDetail--outDir dist

A dist folder will be created containing the generated orderDetail.js file.

--module

This specifies the module format that the generated JavaScript should use. The default is the CommonJS module format if ES3 or ES5 are targeted. ES6 and ESNext are common options today when creating a new project.

--allowJS

This option tells the TypeScript compiler to process JavaScript files as well as TypeScript files. This is useful if we've written some of our code in JavaScript and used features that haven't been implemented yet in all browsers. In this situation, we can use the TypeScript compiler to transpile our JavaScript into something that will work with a wider range of browsers.

--watch

This option makes the TypeScript compiler run indefinitely. Whenever a source file is changed, the compiling process is triggered automatically to generate the new version. This is a useful option to switch on during our developments:

  1. Let's give this a try by entering the following in a terminal:
tsc orderDetail --watch
  1. The compiler should run and, when completed, give the message Watching for file changes. Let's change the getTotal method in the OrderDetail class to handle situations when discount is undefined:
getTotal(discount: number): number {
  const priceWithoutDiscount = this.product.unitPrice * this.quantity;
  const discountAmount = priceWithoutDiscount * (discount || 0);
  return priceWithoutDiscount - discountAmount;
}
  1. When we save orderDetail.ts, the compiler will say File change detected. Starting incremental compilation... and carry out the compilation.

To exit the watch mode, we can kill the terminal by clicking the bin icon in the Terminal.

--noImplicitAny

This forces us to explicitly specify the any type where we want to use it. This forces us to think about our use of any and whether we really need it.

Let's explore this with an example:

  1. Let's add a doSomething method to our OrderDetail class that has a parameter called input with no type annotation:
export class OrderDetail {
  ...
  doSomething(input) {
    input.something();
    return input.result;
  }
}
  1. Let's do a compilation with the --noImplicitAny flag in the Terminal:
tsc orderDetail --noImplicitAny

The compiler outputs the following error message because we haven't explicitly said what type the input parameter is:

orderDetail.ts(14,15): error TS7006: Parameter 'input' implicitly has an 'any' type.
  1. We can fix this by adding a type annotation with any or, better still, something more specific:
doSomething(input: {something: () => void, result: string}) {
  input.something();
  return input.result;
}

If we do a compilation with --noImplicitAny again, the compiler is happy.

--noImplicitReturns

This ensures we return a value in all branches of a function if the return type isn't void.

Let's see this in action with an example:

  1. In our OrderDetail class, let's say we have the following implementation for our getTotal method:
getTotal(discount: number): number {
  if (discount) {
    const priceWithoutDiscount = this.product.unitPrice * this.quantity;
    const discountAmount = priceWithoutDiscount * discount;
    return priceWithoutDiscount - discountAmount;
  } else {
    // We forgot about this branch!
  }
}
  1. We've forgotten to implement the branch of code that deals with the case where there is no discount. If we compile the code without the --noImplicitReturns flag, it compiles fine:
tsc orderDetail
  1. However, let's see what happens if we compile the code with the --noImplicitReturns flag:
tsc orderDetail --noImplicitReturns

We get the following error, as expected:

orderDetail.ts(9,31): error TS7030: Not all code paths return a value.

--sourceMap

When this is set, *.map files are generated during the transpilation process. This will allow us to debug the TypeScript version of the program (rather than the transpiled JavaScript). So, this is generally switched on during development.

--moduleResolution

This tells the TypeScript compiler how to resolve modules. This can be set to classic or node. If we are using ES6 modules, this defaults to classic, which means the TypeScript compiler struggles to find third-party packages such as Axios. So, we can explicitly set this to node to tell the compiler to look for modules in "node_modules".

tsconfig.json

As we have seen, there are lots of different switches that we can apply to the compilation process, and repeatedly specifying these on the command line is a little clunky. Luckily, we can specify these options in a file called tsconfig.json. The compiler options we have looked at in previous sections aredefined in a compilerOptions field without the "--" prefix.

Let's take a look at an example:

  1. Let's create a tsconfig.json file with the following content:
{
  "compilerOptions": {
    "target": "esnext",
    "outDir": "dist",
    "module": "es6",
    "moduleResolution": "node",
    "sourceMap": true,
    "noImplicitReturns": true,
    "noImplicitAny": true
  }
}
  1. Let's run a compile without specifying the source file and any flags:
tsc

The compilation will run fine, with the transpiled JavaScript being output to the dist folder along with a source map file.

Specifying files for compilation

There are several ways to tell the TypeScript compiler which files to process. The simplest method is to explicitly list the files in the files field:

{
  "compilerOptions": {
    ...
  },
"files": ["product.ts", "orderDetail.ts"]
}

However, that approach is difficult to maintain as our code base grows. A more maintainable approach is to define file patterns for what to include and exclude with the include and exclude fields.

The following example looks at the use of these fields:

  1. Let's add the following include fields, which tell the compiler to compile TypeScript files found in the src folder and its subfolders:
{
  "compilerOptions": {
    ...
  },
"include": ["src/**/*"]
}
  1. At the moment, our source files aren't in a folder called src, but let's run a compile anyway:
tsc
  1. As expected, we get No inputs were found in the config file... from the compiler.

Let's create an src folder and move orderDetail.ts into this folder. If we do a compile again, it will successfully find the files and do a compilation.

So, we have lots of options for adapting the TypeScript compiler to our particular situation. Some options, such as --noImplicitAny, force us to write good TypeScript code. We can take the checks on our code to the next level by introducing linting into our project, which we'll look at in the next section.

 

TypeScript linting


As we have seen, the compiler does lots of useful checks against our TypeScript code to help us write error-free code. We can take this a step further and lint the code to help us make our code even more readable and maintainable. TSLint is a linter that is very popular in TypeScript projects, and we will explore it in this section.

The home page for TSLint is at https://palantir.github.io/tslint/.

We'll install TSLint in the next section.

Installing TSLint

We'll install TSLint in this section, along with a Visual Studio Code extension that will highlight linting problems right in the code:

  1. Let's install TSLint globally via npm, as follows:
npm install -g tslint
  1. Now, we can open Visual Studio Code and go to the extensions area (Ctrl + Shift + X) and typetslint in the search box at the top-left. The extension is called TSLint and was published by egamma:
  1. We need to click the Install option to install the extension.
  2. After it has been installed, we'll need to reload Visual Studio Code for the extension to become enabled.

Now that this extension is installed, along with TSLint globally, linting errors will be highlighted right in our code, as we'll see in the following sections.

Configuring rules

The rules that tslint uses when checking our code are configurable in a file called tslint.json. In order to explore some of the rules, we first need a TypeScript file:

  1. So, let's create a file called orderDetail.ts with the following content in Visual Studio Code:
export interface Product {
  name: string;
  unitPrice: number;
}

export class OrderDetail {
  product: Product;
  quantity: number;
  getTotal(discount: number): number {
    const priceWithoutDiscount = this.product.unitPrice * this.quantity;
    const discountAmount = priceWithoutDiscount * discount;
    return priceWithoutDiscount - discountAmount;
  }
}
  1. Let's now create a tslint.json file. We define the rules we want to implement in a rules field. Let's add the following rule:
{
  "rules": {
    "member-access": true
  }
}
  1. A full list of the rules can be found at: https://palantir.github.io/tslint/rules/. The member-access rule forces us to explicitly declare the access modifier for classes. We haven't explicitly defined the property and method access modifiers in the OrderDetail class because they are public by default. So, with our linting rule in place, Visual Studio Code will highlight the lack of access modifiers to us:
  1. As we put a public access modifier in front of the properties and method, the warnings go away:
export class OrderDetail {
public product: Product;
public quantity: number;
public getTotal(discount: number): number {
    const priceWithoutDiscount = this.product.unitPrice * this.quantity;
    const discountAmount = priceWithoutDiscount * discount;
    return priceWithoutDiscount - discountAmount;
  }
}

The member-access rule forces us to write more code – how can this be a good thing? The rule is useful if you're reading the code and don't know TypeScript well enough to understand that class members without access modifiers are public. So, it's great if our team consists of developers who don't know TypeScript that well yet, but not necessarily for an experienced team of TypeScript developers.

Lots of the tslint rules are like member-access – in some teams, they will work well and in others, they don't really add value. This is why rules are configurable!

Built-in rules

tslint has a handy collection of built-in rulesets that can be used. We can use these by specifying the ruleset name in the extends field. We can use multiple rulesets by putting all their names in the array:

  1. Let's adopt the opinionated set of rules that tslint ships with, called "tslint:recommended". So, in our tslint.json file, let's remove the rules field and add an extends field, as follows:
{
  "extends": ["tslint:recommended"]
}

We immediately get lint errors when tslint.json is saved. The error is complaining about the lack of an I prefix on our Product interface. The logic behind the rule is that, while reading code, if a type starts with an I, we immediately know that it is an interface.

  1. Let's pretend that this rule isn't valuable to us. We can override this rule from "tslint:recommended" in the "rules" field. The rule is called "interface-name". So, let's override this to false:
{
  "extends": ["tslint:recommended"],
  "rules": {
"interface-name": false
  }
}

When tslint.json is saved, the linting errors immediately go away.

Excluding files

We can exclude files from the linting process. This is useful for excluding third-party code. We do this by specifying an array of files in an exclude field in the linterOptions field:

{
 "extends": ["tslint:recommended"],
"linterOptions": {
 "exclude": ["node_modules/**/*.ts"]
 }
}

The preceding configuration excludes third-party node packages from the linting process.

Now that we've added TSLint to our tool belt, we are going to add another tool that will automatically format our code for us. This will help our code adhere to some of the code formattings TSLint rules.

 

Code formatting


In this section, we are going to install another extension in Visual Studio Code, called Prettier, which will automatically format our code. As well as putting a stop to all the ongoing debates over styles, it will help us adhere to some of the TSLint rules:

  1. Let's open Visual Studio Code, go to the Extensions area, and type prettier in the search box. The extension is called Prettier - Code formatter and was published by Esben Petersen:
  1. We need to click the Install option to install the extension.
  2. After it has been installed, we'll need to reload Visual Studio Code for the extension to become enabled.
  3. The last step is to make sure the Format on Save option is ticked in User Settings. Press Ctrl+, (comma) to open the settings screen and type Format On Save in the search box to find the setting. If the setting isn't ticked, then tick it:

Now that this extension is installed, when we save our TypeScript code, it will automatically be formatted nicely for us.

 

Summary


At the start of this chapter, there was a section on why we would use TypeScript to build a frontend. We now have first-hand experience of TypeScript catching errors early and giving us productivity features such as IntelliSense. We learned that TypeScript is just an extension of JavaScript. So, we get to use all of the features in JavaScript plus additional stuff from TypeScript. One of these additional things is type annotations, which help the compiler spot errors and light up features such as code navigation in our code editor.

We haven't covered everything about types yet, but we have enough knowledge to build fairly complex TypeScript programs now. Classes, in particular, allow us to model complex real-world objects nicely. We learned about modules and how they keep us out of that dangerous global scope. Modules allow us to structure code nicely and make it reusable. We can even use these if we need to support IE, because of that magical TypeScript compiler.

We learned a fair bit about the TypeScript compiler and how it can work well in different use cases because it is very configurable. This is going to be important for when we start to use TypeScript with React later in the book.

TSLint and Prettier were the icings on the cake. It's down to us and our team to debate and decide the TSLint rules we should go with. The benefit of both these tools is that they force consistency across our code base, which makes it more readable.

Now that we understand the basics of TypeScript, we'll dive into the new features that have been added in TypeScript 3.

 

Questions


Here are some questions to test what you have learned in this first chapter. The answers can be found in the appendix.

Good luck!

  1. What are the 5 primitive types?
  2. What would the inferred type be for the flag variable be in the following code?
const flag = false;
  1. What's the difference between an interface and a type alias?
  2. What is wrong with the following code? How could this be resolved?
class Product {
  constructor(public name: string, public unitPrice: number) {}
}

let table = new Product();
table.name = "Table";
table.unitPrice = 700;
  1. If we want our TypeScript program to support IE11, what should the compiler--target option be?
  2. Is it possible to get the TypeScript compiler to transpile ES6 .js files? If so, how?
  3. How can we prevent console.log() statements from getting into our code?
 

Further reading


http://www.typescriptlang.org has great documentation on TypeScript. It is worth looking at the following pages of this site to cement your knowledge, or using them as a quick reference guide:

The full list of tslint rules can be found at https://palantir.github.io/tslint/rules/.

About the Author

  • Carl Rippon

    Carl Rippon has been involved in the software industry for over 20 years, developing a complex line of business applications in various sectors. He has spent the last eight years building single page applications using a wide range of JavaScript technologies, including Angular, ReactJS, and TypeScript. Carl has written over 100 blog posts on various technologies.

    Browse publications by this author

Latest Reviews

(5 reviews total)
Até o momento o livro é fantástico, aborda desde os conceitos iniciais de Typescript até os mais avançados. Os exemplos são práticos e didáticos.
Die Lieferung dauerte 14 Tage! Ist das noch zeitgemäß? Wie lange dauert es nach dem Brexit? 30 Tage?
I wanted to buy a book, I submitted my individual discount code that You have sent me by e-mail, and first time I got information that coupon is applied, but price of my order didn't change. Next tries of applying mycoupon was resulting with information that coupon is invalid. I sent an email to Packt Customer Care with description of all the situation and I also attatched screenshot. I received an answer that coupon is valid only for e-books, and mail with individual discount code didn't mention that at all. It said that discount is for all the titles. What is the point of generation individual discount codes and make clients confused if at the same time there was global offer on packtpub site for everybody with same result. I felt disappointed because I wanted my books to be printed (phisical) ones, so I could get them with myself on a vacation and I had to buy ebooks which I must read at work on computer, because I dont want to buy ebook reader for two books. If you understand my position then you understand why I gave 3 stars.

Recommended For You

Book Title
Unlock this full book FREE 10 day trial
Start Free Trial