Book Image

Learn React with TypeScript 3

By : Carl Rippon
Book Image

Learn React with TypeScript 3

By: Carl Rippon

Overview of 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.
Table of Contents (14 chapters)

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, the compiler 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

--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 are defined 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.