0% found this document useful (0 votes)
107 views172 pages

Beginning TypeScript A Step-By-Step Gentle Guide To Master TypeScript For Beginners (Sebhastian, Nathan)

This document is a comprehensive guide to learning TypeScript, covering its fundamentals, advantages, and integration with popular frameworks like Node.js and React. It includes step-by-step instructions for setting up the development environment, writing TypeScript code, and understanding key concepts such as static typing and transpilation. The book is structured into 15 chapters, each focusing on different aspects of TypeScript programming, making it suitable for beginners and those looking to enhance their skills.

Uploaded by

mingcoder2025
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
107 views172 pages

Beginning TypeScript A Step-By-Step Gentle Guide To Master TypeScript For Beginners (Sebhastian, Nathan)

This document is a comprehensive guide to learning TypeScript, covering its fundamentals, advantages, and integration with popular frameworks like Node.js and React. It includes step-by-step instructions for setting up the development environment, writing TypeScript code, and understanding key concepts such as static typing and transpilation. The book is structured into 15 chapters, each focusing on different aspects of TypeScript programming, making it suitable for beginners and those looking to enhance their skills.

Uploaded by

mingcoder2025
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 172

TABLE OF CONTENTS

Preface
Working Through This Book
Requirements
Contact
Chapter 1: Introduction to TypeScript
What is TypeScript?
Why Use TypeScript?
When to Use TypeScript?
Development Environment Set Up
Summary
Chapter 2: Your First TypeScript Program
Transpiling TypeScript into JavaScript Files
TypeScript Configuration File
Summary
Chapter 3: TypeScript Overview
The Basic Types
Type Inference
The any Type
Array Types
Code Completion
Summary
Chapter 4: Typing Functions
The void Type
Optional Parameters
Adding a Default Value
Rest Parameters
Typing Function Signature
Summary
Chapter 5: Objects and Custom Types
Typing Objects
Optional Properties
Creating a Read Only Property
Creating a Custom Type
Index Signature
Summary
Chapter 6: Advanced Types
Union Type
Intersection Type
Literal Type
The Unknown Type
The Never Type
The Satisfies Operator
Summary
Chapter 7: Advanced Object Typing
The 'keyof' Operator
Type Mapping
Tuples
Enums
Summary
Chapter 8: Typing Classes and Object-Oriented Programming
How to Create a Class
Defining Class Methods
Why a Function in Object/Class Is Called a Method?
Inheritance
Overriding methods
Object-Oriented Programming Paradigm
Summary
Chapter 9: Advanced Class Features (Part 1)
Value Modifiers
Visibility (or Access) Modifiers
Parameter Properties Shorthand
Getters and Setters
Summary
Chapter 10: Advanced Class Features (Part 2)
Static Modifier
The Abstract Modifier
Interfaces
Interface vs Type - Which One to Use?
Summary
Chapter 11: Dynamic Typing With Generics
Generic Functions
Generic Types
Generic Classes
Generic Conclusion
Summary
Chapter 12: Modules, Type Assertion, and Discriminating
Unions
Modules in TypeScript
Type Assertion Using the 'as' Operator
Discriminating Unions
Summary
Chapter 13: Integrating TypeScript in JavaScript Projects
Using JSDoc to Check JavaScript Files
JSDoc Disable Type Checking
Declaration Files
Using Declaration Files From Definitely Typed Library
Summary
Chapter 14: TypeScript in Node.js and Express
Creating a Node.js Project
Configuring TypeScript
Creating a Basic Express Server
Creating an API Route
Typing Request Body With DTOs
Modeling Data With Classes
Testing API Routes With Bruno
Adding PATCH and DELETE Routes
Summary
Chapter 15: TypeScript in React
Creating a React Application
Explaining TypeScript in React
Adding Bootstrap for Styling
Modeling Data in React
Passing Generic Type to useState Hook
Creating the Service Object
Fetching Data With useEffect Hook
Deleting Task
Adding Task
Summary
Wrapping Up
About the author
Beginning TypeScript
A Step-By-Step Gentle Guide to Master TypeScript for Beginners

By Nathan Sebhastian
PREFACE

The goal of this book is to provide a gentle step-by-step


instructions that help you learn TypeScript gradually from
basic to advanced.

You will see why many developers prefer to work in TypeScript


over JavaScript.

We’ll cover essential TypeScript topics such as typing variables,


arrays, objects, functions, classes, and see how they are used to
guard against errors that usually occur when coding in
JavaScript.

After finishing this book, you will know how to use TypeScript
and integrate it into the most popular web development
libraries and frameworks such as Node.js/Express and React.

Working Through This Book


This book is broken down into 15 concise chapters, each
focusing on a specific aspect of TypeScript programming.
I encourage you to write the code you see in this book and run
them so that you have a sense of how coding in TypeScript
looks like. You learn best when you code along with examples
in this book.

A tip to make the most of this book: Take at least a 10-minute


break after finishing a chapter, so that you can regain your
energy and focus.

Also, don’t despair if some concept is hard to understand.


Learning anything new is hard for the first time, especially
something technical like programming. The most important
thing is to keep going.

Requirements
To experience the full benefit of this book, basic knowledge of
JavaScript is required.

If you need some help in learning JavaScript, you can get my


book at https://codewithnathan.com/beginning-modern-
javascript

Contact
If you need help, you can contact me at
[email protected].

You can also connect or follow me on LinkedIn at


https://www.linkedin.com/in/nathansebhastian/
CHAPTER 1: INTRODUCTION TO
TYPESCRIPT

In this chapter, you’re going to learn what is TypeScript, why


use TypeScript, and when to use it in your projects.

You’re also going to install the programs required to run


TypeScript on your computer, then create and execute your
first TypeScript program.

Let’s jump in and get started!

What is TypeScript?
TypeScript is an open-source programming language developed
by Microsoft that adds a typing system to JavaScript.

Basically, TypeScript is an extension of JavaScript. It has the


same syntax as JavaScript. It has all of JavaScript features, and
every JavaScript .js file is a valid TypeScript file.

TypeScript can’t be executed on JavaScript environment such as


the browser or Node.js. When you want to deploy the
application, you need to transform TypeScript into JavaScript
using the TypeScript compiler program.

The typing system in TypeScript changes JavaScript into a


statically typed language, which enables advanced code
completion and error detection in development.

I’ll show you how advanced code completion and error


detection work in the following chapters.

For now, let’s focus on understanding what is a statically typed


language.

Why Use TypeScript?


Based on its typing system, programming languages are divided
into two categories:

1. Statically typed language


2. Dynamically typed language

In a statically typed language, the type of every single variable


used in your code is known before you even run the code.

For example, this is how you create an integer variable in Java:

int myNumber = 2;

Notice that the type of the variable int is defined in front of the
variable named myNumber.
When you further develop the application, the type of the
myNumber variable CAN’T change, or Java will throw an error:

int myNumber = 2;

myNumber = 10; // OK

myNumber = "abc"; // ERROR

Examples of statically typed languages are Java, C#, and Swift.

By contrast, when you’re writing in a dynamically typed


language, the type of the variable CAN change when you run
the code.

In JavaScript, you can change a number variable into a string


variable without any issue:

let myNumber = 2;

myNumber = "abc"; // OK

Examples of dynamically typed languages are Python, Ruby,


and of course, JavaScript.

A dynamically typed language is flexible and non-verbose. You


don’t need to type as much code as a statically typed language,
and this means you can be more productive when compared to
a static type language.

But there are disadvantages for using the speed and simpler
syntax of a dynamic type language.

For example, what if you pass a string into a function that


expects a number?
In JavaScript, the function simply returns a NaN as follows:

let myNumber = 2;

myNumber = "abc";

Math.round(myNumber); // NaN

But is this true? Would you like a NaN when you expect a
rounded number when calling the round() function?

Now when you code in Java the IDE you use would already
warn you of an error before you run the code.

You would get red squiggly lines below the error like this:

And when you try to compile the code to run it, Java’s compiler
program won’t work because the error prevents the
compilation.

A static type language notifies you about an error or any other


possible issue in your code without needing to run the code at
all.

When you use TypeScript, you essentially turn JavaScript into a


static type language.
You can’t change the type of your variables after it has been
declared, except by explicitly converting them.

When passing arguments to a function, you need to pass


arguments of the right types, or there will be red squiggly lines
in your code.

Sure, there’s more code to write when you use a static type
language, but the cost is outweighed by the benefits:

1. Type checking reduces the possibility of error when


executing code
2. Adding types enable code completion (more on this
later)
3. Your code is easier to read and understand

TypeScript makes working in JavaScript predictable and


scalable, and that’s why it’s used on many software projects by
large companies such as Microsoft and Netflix.

When to Use TypeScript?


TypeScript is usually used on medium to large projects, where
there’s a team of developers maintaining and developing
features on a daily basis.

If you’re working alone on a small project, it’s still totally cool to


use JavaScript. You probably benefit from the dynamic typing
and short syntax more than the additional type system added
by TypeScript.
But in all honesty, you can use TypeScript whenever you want
because TypeScript has an incremental adoption strategy baked
into the language itself.

This means you can convert a JavaScript project into a


TypeScript project incrementally, step by step as you need it.

I’ll show you how this is done in the following chapters. For
now, let’s set up your computer so that you can create and run a
TypeScript program.

Development Environment Set Up


To start programming in TypeScript, you need to have three
things on your computer:

1. A web browser
2. A code editor
3. The Node.js program

Let’s install them in the next section.

Installing Chrome Browser


Any web browser can be used to browse the Internet, but for
development purposes, you need to have a browser with
sufficient development tools.

The Chrome browser developed by Google is a great browser


for web development, and if you don’t have the browser
installed, you can download it here:
https://www.google.com/chrome/

The browser is available for all major operating systems. Once


the download is complete, follow the installation steps
presented by the installer to have the browser on your
computer.

Next, we need to install a code editor. There are several free


code editors available on the Internet, such as Sublime Text,
Visual Studio Code, and Notepad++.

Out of these editors, my favorite is Visual Studio Code because


it’s fast and easy to use.

Installing Visual Studio Code


Visual Studio Code or VSCode for short is a code editor
application created for the purpose of writing code. Aside from
being free, VSCode is fast and available on all major operating
systems.

You can download Visual Studio Code here:

https://code.visualstudio.com/

When you open the link above, there should be a button


showing the version compatible with your operating system as
shown below:
Figure 1. Downloading VSCode

Click the button to download VSCode, and install it on your


computer.

Now that you have a code editor installed, the next step is to
install Node.js

Installing Node.js
Node.js is a JavaScript runtime application that enables you to
run JavaScript outside of the browser. We need this program to
install the TypeScript compiler.

You can download and install Node.js from https://nodejs.org.


Pick the recommended LTS version because it has long-term
support. The installation process is pretty straightforward.
To check if Node has been properly installed, type the command
below on your command line (Command Prompt on Windows
or Terminal on Mac):

node -v

The command line should respond with the version number of


the Node.js you have on your computer.

Node.js also includes a program called npm (Node Package


Manager) which you can use to install and manage Node
packages:

npm -v

Node packages are JavaScript libraries and frameworks that


you can use for free in your project. We’re going to use npm to
install TypeScript compiler later.

Now you have all the software needed to start programming in


TypeScript. Let’s do that in the next chapter.

Summary
In this chapter, you’ve learned that TypeScript adds static
typing on top of JavaScript, why it’s used by serious software
companies around the world, and when you might want to use
it.

You’ve also installed the tools required to write TypeScript code


on your computer.
If you encounter any issues, you can email me at
[email protected] and I will do my best to help you.
CHAPTER 2: YOUR FIRST
TYPESCRIPT PROGRAM

It’s time to create your first TypeScript program.

First, you need to install the TypeScript compiler using npm. On


your terminal, run the command below:

npm install -g typescript

The npm install command is used to install the package you


specify next to it.

Here, we install the typescript package, which is the only thing


we need to start programming in TypeScript.

The -g option is short for global installation. This is used so that


we can call the TypeScript compiler from the command line
directly.

Once the installation is finished, you can verify that the package
is installed correctly by checking its version:

tsc -v
Version 5.4.5

The version you have might be newer, but don’t worry because
I will make sure that the topics covered from this point remain
relevant and updated for big changes.

Next, create a folder on your computer that will be used to store


all files related to this book. You can name the folder
'beginning_typescript'.

Next, open the Visual Studio Code, and select File > Open
Folder…​ from the menu bar. Select the folder you’ve just
created earlier.

VSCode will load the folder and display the content in the
Explorer sidebar, it should be empty as we haven’t created any
files yet.

To create a file, right-click anywhere inside the VSCode window


and select New Text File or New File…​from the menu.

Once the file is created, press Control + S or Command + S to save


the file. Name that file as index.ts because .ts is the TypeScript
file format.

The next step is to put some content in the file. First, let’s create
a variable and annotate its type as a number:

let myNumber :number = 10;

When using TypeScript, you can specify the type of a variable


with :type annotation after the variable name.
The above type annotation :number sets the type of the myNumber
variable as a number.

If you try to assign a string value to the variable, you’ll get the
red squiggly lines as shown below:

let myNumber: number = 10;

myNumber = "abc";
~~~~~~~~

console.log(myNumber);

If you hover your mouse over the code with the red squiggly
lines, you’ll see an error detail pop up as follows:

Figure 2. Red Squiggly Lines in TypeScript

TypeScript has been integrated well with VSCode because both


are developed by Microsoft. That said, you should still see the
same error when you use a code editor that supports
TypeScript.

This is one of the benefits of using TypeScript. Instead of finding


errors only after you run the code, TypeScript can look into the
code written in your .ts files, then detect any error before you
even run them.
Transpiling TypeScript into JavaScript
Files
Transpilation is the process of transforming code from one
language to another language.

The TypeScript syntax is known to code editors such as VSCode,


but it’s an unknown language for JavaScript runtime
environment such as the browser and Node.js.

TypeScript needs to be transpiled to JavaScript to run the


program, so let’s use the tsc program we installed earlier to do
it.

Open the terminal on your TypeScript folder, then run the tsc
command followed by the .ts file name as follows:

tsc index.ts

You’ll see an error on the terminal, but TypeScript still


generates an index.js file anyway.

You can actually instruct TypeScript to abort compilation when


there’s an error. I’ll show you how in the next section.

For now, open the index.js file to see the code generated by the
compiler:

var myNumber = 10;


myNumber = "abc";
console.log(myNumber);
Now this is the usual JavaScript code that can be executed by
JavaScript environment.

TypeScript is only used in the coding phase, and it always needs


to be converted to JavaScript using the compiler before
running.

TypeScript Configuration File


In the generated .js file above, you can see that TypeScript uses
var instead of let for the variable because it tries to keep the
code compatible with older environments.

You can actually tell TypeScript to use let and const for the
generated code by creating a TypeScript configuration in the
form of a JSON file.

Back to the terminal, run the command below:

tsc --init

The --init option will initialize and create a new tsconfig.json


file in your project.

You should see the output as follows:

Created a new tsconfig.json with:

target: es2016
module: commonjs
strict: true
esModuleInterop: true
skipLibCheck: true
forceConsistentCasingInFileNames: true
You can learn more at https://aka.ms/tsconfig

The options above are the defaults enabled by TypeScript.

Open the tsconfig.json file, and please don’t be intimidated by


the massive number of options you see there.

The tsc --init command generates all possible options you can
define in the JSON file, but you only need to learn a handful of
them to use TypeScript effectively.

Each options have some comment explaining what they do, so


let’s explore some of the most important ones:

target is the JavaScript version for the generated code. This is


set to es2016 because that’s the version implemented in all
browsers.

You can set a higher version if you want. To see all available
options, delete the value es2016 then press Control + Space.
Here’s an example:
If you’re not sure what target to specify, then just use the
default value at es2016.

Next, there’s the module option which sets the JavaScript module
system. Setting to commonjs will use the module.exports and
require() syntax, while setting to any ES version will use the
export and import syntax.

If you use Node.js to run JavaScript code, it’s better to leave it as


commonjs. Node.js supports ES module syntax, but you have to
set the type: module option in the package.json file.

Below the module option, there’s the rootDir option, which sets
the root directory of your source code.

This is a useful option to separate the TypeScript and JavaScript


files, so let’s uncomment this option and set it to ./src as
follows:

"rootDir": "./src"
Next, create the src/ folder in your project, and move the
index.ts file there.

Then, open VSCode search box by pressing Control + F or


Command + F and look for the outDir option.

This option specifies where TypeScript should generate the


output of the transpilation. Set it to ./dist as follows:

"outDir": "./dist"

You can delete the index.js file from the project for now.

After that, uncomment the removeComments option so that all


comments in the TypeScript files won’t be preserved in
JavaScript files.

The last option you need is the noEmitOnError option, which


stops the transpilation when there’s an error in the TypeScript
files.

With these options, now you can run TypeScript compiler


without specifying the file:

tsc

The tsc command will look into tsconfig.json file and use your
rootDir option to find the .ts files.

Because there’s an error, no JavaScript files will be emitted. You


need to delete the myNumber = "abc"; line from the .ts file:

// index.ts
myNumber = "abc"; // Delete or comment this line

Now TypeScript will create the dist/ folder and place the
generated index.js file there. Good work!

Summary
In this chapter, you’ve created and run your first TypeScript
program. You’ve also seen how TypeScript static typing causes
an error when you assign a string to a number type variable.

Then, you learned how to generate a tsconfig.json file and


activated some of the most useful TypeScript options, such as
rootDir, outDir, and noEmitOnError.

We will activate more options as we go through the following


chapters.
CHAPTER 3: TYPESCRIPT
OVERVIEW

In the previous chapter, you’ve seen how TypeScript can be


used to annotate a variable with a specific type.

TypeScript is an extension of JavaScript, which means you can


use all JavaScript built-in data types to annotate your variables.

Before we dive deep into TypeScript, let’s start by exploring the


core features and how they enhance the development
experience over plain JavaScript.

The Basic Types


Using TypeScript, you can use all available JavaScript types
when declaring your variable:

let myString: string = "abc";

let myNumber: number = 123;

let myBoolean: boolean = true;

let myNull: null = null;


let myUndefined: undefined = undefined;

The string, number, and boolean types can hold any value of the
respective type, but null and undefined can only hold that
specific value.

Aside from JavaScript built-in types, here’s a list of basic types


provided by TypeScript:

​ any - Any value is valid



​▪​ unknown - Similar to any, but you need to assert or narrow the
type before performing any operation on it

​▪​ void - No value. Used to type return value of functions

​ never - For representing unreachable code



We will gradually look into these TypeScript types in the


following chapters.

For now, let’s look into inferred types.

Type Inference
TypeScript can get the type of your variable based on the value
you assign to that variable.
For example, suppose you assign a string to a variable like this:

let myString = "abc";

TypeScript finds that the value of myString is a string, so it will


annotate the variable as a :string type.

This process of setting the type based on the value is called type
inference (or type guessing)

In VSCode, you can see the inferred type by hovering over the
variable as shown below:

You can see that the myString variable is annotated as :string


type using type inference.

The any Type


The any type is one of the new types added in TypeScript. This
type can represent any value: a string, boolean, number, or
anything else.

This type is inferred when you declare a variable without


assigning any value like this:

let myVariable;
Now hover over the variable to see the any type inferred as
follows:

When a variable is annotated as any type, that variable can be


assigned any type of value:

let myVariable;

myVariable = 'abc';

myVariable = true;

Using the any type makes the variable a dynamic type again,
and this goes against the purpose of using TypeScript.

Unless you have a strong reason, you really shouldn’t use the
any type in your code.

Array Types
In JavaScript, an array can contain elements of mixed types.
You can have both a number and a string in your array as
follows:

let myArray = [1, '2'];


When using TypeScript, an array can be forced to contain
elements of a single type by annotating the variable with
:type[] syntax.

The square brackets next to the type indicate that the variable
must be an array. Here’s an example:

let myArray: number[] = [1, 2];

By using the :number[] annotation, TypeScript will complain


when you add an element that’s not of number type.

If you don’t annotate the variable, then TypeScript will infer the
types that you can put in the array from the declaration.

If you declare the variable with an array of number and string,


then the inferred type is a union of number and string as
follows:

let myArray = [1, '2']; // (string | number)[]

The pipe | symbol in TypeScript stands for union.

In the code above, the (string | number)[] type means that the
type is an array that may contain a string or a number.

You will learn more about union type later. The important point
here is that TypeScript can force an array to have only a single
type.

When you declare an empty array, then TypeScript will guess


the type as any[]:
let myArray = []; // any[]

This is not recommended because you can then add any values
to the array.

When you want to initialize an empty array, it’s better to always


annotate the type as shown below:

let myArray: string[] = [];

This way, only values of a specific type can be added to the


array.

Code Completion
Aside from just adding types to your JavaScript code, TypeScript
also has a code completion feature.

To show you an example, let’s create an array of strings, then


iterate over the array using the forEach() method as shown
below:

let myArray: string[] = ['a', 'b', 'c'];

myArray.forEach(element => element)

Now inside forEach callback function, the moment you type a


dot . next to the element in the body, you’ll have a window pop
up as follows:
The auto complete shows all properties and methods you can
access from the element string object.

Because we annotate the variable as a string array, the code


editor can provide this auto complete feature.

When we set the type as any[], the auto complete window won’t
show up:

let myArray: any[] = ['a', 'b', 'c'];

myArray.forEach(element => element.??)


When using plain JavaScript, the values defined in an array can
be anything, so the editor doesn’t try to provide you with
helpful options.

The auto complete works not only for arrays, but for any built-
in and custom types that you might define in your code.

This is one of the most loved features of TypeScript because it


greatly boosts your productivity as a developer.

Summary
In this chapter, you’ve learned how TypeScript can be used to
annotate variables using the built-in JavaScript types.

TypeScript also introduces a new type called any, which can


contain a value of any type. This type isn’t recommended for
use, except as a temporary solution that you’re going to fix
later.

You’ve also learned how to annotate an array, and how


TypeScript provides an auto complete to boost your
productivity.

Next, let’s see how we can annotate functions using TypeScript.


CHAPTER 4: TYPING FUNCTIONS

Functions are the most common thing we’re going to use when
programming in JavaScript, so we’re going to explore how to
type functions first.

When defining a function, you can add annotation for the


function parameters and return value.

To annotate a parameter, add the :type after the parameter


name. The code below shows how to annotate two number
parameters:

function calculateProfit(revenue: number, expense: number) {}

The revenue and expense parameters above are both the number
type.

Next, you can also annotate the return value after the
parentheses as shown below:

function calculateProfit(revenue: number, expense: number) :number {}

Notice the :number type after the parentheses above. This


indicates the type of the return value.
TypeScript will complain unless you return a number from the
function:

function calculateProfit(revenue: number, expense: number) :number {


return revenue - expense;
}

Type inference also applies to functions. When you define a


parameter without annotation, TypeScript will infer the type as
any, and this is not allowed:

When defining a function, you always need to annotate the type


of the parameters explicitly.

You can break this rule by setting the noImplicitAny rule to


false in your tsconfig.json file. But again, it’s not
recommended to use any in your source code.

As for the return type, it can still be inferred from the function’s
return statement. See the code below:

// Same as function calculateProfit(): number


function calculateProfit() {
return 100;
}

You can check the returned value type by hovering over the
function name.
Because we return a number, TypeScript infers the return value
as such.

The void Type


The void type is one of the new types added in TypeScript.

This type is used to indicate that the function we write returns


nothing. For example:

function greet(): void {


console.log('Hello World!');
}

The greet function above only performs a console log, so there’s


no return value needed.

Note that the void type isn’t something you use when declaring
a variable. It’s only used for annotating a function’s return
value as nothing.

Optional Parameters
When you call a function without passing all specified
parameters, TypeScript will show an error:

function calculateProfit(revenue: number, expense: number) {


return revenue - expense;
}

calculateProfit(100);
~~~~~~~~~~~~~~~
// ^ An argument for 'expense' was not provided.
You can create an optional parameter in TypeScript functions
by adding a ? after the parameter name.

For example, you can make the expense parameter optional in


the calculateProfit() function like this:

function calculateProfit(revenue: number, expense? :number) {


return revenue - expense;
~~~~~~~
// ^ 'expense' is possibly 'undefined'.
}

Because the expense parameter is now optional, TypeScript is


going to complain that expense can be undefined.

You can fix this by adding an if block to check if the expense


parameter is defined before using it:

function calculateProfit(revenue: number, expense?: number) {


if (expense) {
return revenue - expense;
}
return revenue;
}

Now when you call the function, you can omit the expense
parameter:

calculateProfit(100, 36);
calculateProfit(100);

If you code in plain JavaScript, then calling the function without


passing expense and checking it with the if statement causes the
function to return a NaN:
function calculateProfit(revenue, expense) {
return revenue - expense;
}

calculateProfit(100); // NaN

TypeScript reduces the possibility of getting a NaN like this in


your code.

Adding a Default Value


Another way to make a parameter optional is to add a default
value to your parameter.

For example, here’s how to specify 20 as the default value of the


expense parameter:

function calculateProfit(revenue: number, expense = 20) {


return revenue - expense;
}

calculateProfit(100);

TypeScript will infer the type of the expense parameter from the
default value, so you don’t need to annotate it.

Also, you don’t need to use an if statement to check the expense


parameter because it will never be undefined.

Rest Parameters
In JavaScript, you might create a function that can accept many
parameters using the rest syntax.
For example, you can have a sum() function that accepts an
infinite number of parameters:

function sum(...numbers){
// the sum operation
}

When you have a rest parameter as shown above, you need to


type the parameter as an array of one type.

numbers parameter can be typed like this:


For example, the …​

function sum(...numbers :number[]){


// the sum operation
}

The rest parameter always returns an array so you can type the
parameter as an array like number[], string[] and so on.

Typing Function Signature


So far, you’ve seen how to type the parameters and return
value of a function.

But what if you want to type the function itself? For example,
you might want to make sure that the function has a specific
amount of parameters that have specific types.

To do this, you can define the function using the arrow function
syntax, then type the function signature next to the function
name.

Here’s an example:
const calculateProfit :(revenue: number, expense: number) => number = (
revenue,
expense
) => {
return revenue - expense;
};

In the code above, the calculateProfit function is created using


the arrow function syntax.

After the function name, we annotate it using the arrow


function syntax.

When annotating a function, you need to specify the parameter


names and types, then define the return type after the arrow ⇒
syntax.

After that, you can start writing the function by using the
assignment = operator.

Notice that the type annotation for the function looks messy
and very verbose.

There’s a better way to type function signature, and I’ll show


you how in the next chapter.

Summary
In this chapter, you’ve learned how to annotate functions in
TypeScript.

By annotating the parameters and return value of a function,


you can reduce the possibility of having errors when running
your code.
You can also annotate a function when using the arrow
function syntax so that the function always conforms to the
type specification.
CHAPTER 5: OBJECTS AND
CUSTOM TYPES

In this chapter, I’m going to show you how to type an object and
create a custom type to make your TypeScript code less verbose
and messy.

Typing Objects
In JavaScript, objects are very dynamic because you can have
properties of any type.

Using TypeScript, you can restrict the type of an object


properties.

For example, suppose you have a user object that has id and
name properties. Here’s how you type the properties:

let user: {
id: number,
name: string
} = {
id: 1,
name: 'Nathan'
};
The properties and their value types can also be inferred from
the default properties, so you can omit the annotation like this:

let user = {
id: 1,
name: 'Nathan'
};

Rather than typing the variable as an object, TypeScript actually


types the variable as a specific shape.

This means you can’t add new properties randomly after


initialization like this:

let user: {
id: number,
name: string
} = {
id: 1,
name: 'Nathan'
};

user.age = 28;
~~~
// ^ Property 'age' does not exist on type

Using TypeScript adds constraint to the shape of your object.

Optional Properties
You can also specify optional properties by adding a question
mark ? after the property name.

In the code below, the name and age properties are made
optional:
let user: {
id: number,
name?: string,
age?: number,
} = {
id: 1
};

// Add optional properties

user.name = 'Nathan';
user.age = 28;

While you can add optional properties after initializing the


object, they are rarely used in real world projects.

Creating a Read Only Property


Sometimes, you have an object with a property that shouldn’t
be modified after initialization.

TypeScript has the readonly type modifier that you can use to
mark a property as read only.

For example, the id property of the user object should be made


read only:

let user: {
readonly id: number,
name: string
} = {
id: 1,
name: 'Nathan'
};
Now you’ll get an error if you try to change the value of id
down the line:

user.id = 2;
~~
// Cannot assign to 'id' because it is a read-only property

Creating a Custom Type


Now when you look at the object annotation, note that it’s kinda
confusing and messy to read.

What’s more, if you want to create another user object, you


need to repeat the annotation in the new object.

To solve these issues, you can define a custom type as a single


source of the type signature, then reuse that custom type for all
objects that has an identical signature.

To create a custom type, use the type keyword provided by


TypeScript as follows:

type User = {
readonly id: number,
name: string
};

A type alias is written in the PascalCase format. The first


character uses a capital letter to make it stand out.

Now whenever you want to create an object of User type, you


can annotate that object as follows:
const sender: User = {
id: 1,
name: 'Nathan'
};

const recipient: User = {


id: 2,
name: 'Anna'
};

A custom type is also known as type alias, and you can also use
it to type a function.

Recall that in the previous chapter, you’ve annotated an arrow


function with its signature as follows:

const calculateProfit :(revenue: number, expense: number) => number = (


revenue,
expense
) => {
return revenue - expense;
};

You can actually create a type for the function signature, then
assign that type to the function like this:

type CalculateProfitFn = (revenue :number, expense :number) => number;

const calculateProfit: CalculateProfitFn = (revenue, expense) => {


return revenue - expense;
}

By separating the type from the definition, you make the type
easier to read and reusable.

A custom type can also be a single type as follows:


type CustomBoolean = boolean;

const myBoolean: CustomBoolean = true;

But this is not very useful, isn’t it? Type alias only shines when
you need to create advanced types, such as an object or a
function.

Index Signature
Index signature is a syntax used to check on the type of the
object properties without restricting the shape of that object.

In the previous examples, you’ve seen how you can define the
shape or structure of an object using a custom type:

type User = {
readonly id: number
name: string
};

But there are times when the structure of an object is unknown


to you.

The object you want to create might have more than just the
two properties described in the custom type.

This is where index signature comes into play. This syntax


enables you to add an infinite number of properties to the
object of a specific type.

To create an index signature, you need to use the syntax [key:


KeyType]: ValueType as follows:
type User = {
[key: string]: string
};

Now when you create an object of type User, you can add more
properties after initialization:

const recipient: User = {


name: 'Anna'
};

// Adding more properties after initialization:


recipient.email = '[email protected]';

// You can also use the square brackets notation


recipient['address'] = 'Flat 49l Pauline Locks';

By using index signature, you can add properties to the object


after initialization, but you still have type checking for those
properties.

In the example above, the property value must be a string


following the index signature value type.

When adding properties to an object, you can use either the dot
. notation or the square brackets [] notation.

When you use an index signature, you can still define


properties that must exist in the object as follows:

type User = {
readonly id: number
name: string
[key: string]: string | number
};

// Create an object of User type:


const recipient: User = {
id: 1,
name: 'Anna'
};

// Adding more properties after initialization:


recipient['email'] = '[email protected]';
recipient['address'] = 'Flat 49l Pauline Locks';

The above definition means the User type can add more
properties after initialization, but the id and name properties are
mandatory.

Because the id property is a number, you must use the union


type string | number as the value type of the index signature.

Summary
You’ve learned quite a lot in this chapter.

Now you know how to type an object, as well as how to create a


custom type (or type alias) to make your types clean and
reusable.

In the next chapter, we’re going to look at advanced TypeScript


features used to create complex types.

See you there!


CHAPTER 6: ADVANCED TYPES

In TypeScript, a type can be as simple or as complex as you


need it to be.

In this chapter, we’re going to look at the advanced types that


TypeScript provides.

Union Type
A union type is a modifier that enables you to put more than
one type for a variable.

We’ve seen a few examples of this type in previous chapters.


One example is when we create an array that can contain a
string or number:

let myArray = [1, '2']; // (string | number)[]

The union type is defined by writing the pipe | or vertical bar


character.

You can use union type when defining a variable or a function


parameter as follows:
function calculateProfit(revenue: number | string){
console.log(revenue);
}

The revenue parameter above can be a number or a string.

If you try to access a property or method of the revenue


parameter, you’ll see that the TypeScript shows only properties
and methods that are available in both number and string types:

If you access a method that doesn’t exist in both types, you’ll get
an error as shown below:

function calculateProfit(revenue: number | string){


revenue.toFixed();
~~~~~~~
}

When specifying a union, TypeScript doesn’t allow you to


access a method or property that’s not available in all types
specified in the union.

This makes sense as the value can be either one of the union
types.

To access a method specific to one type, you need to narrow the


type down using the typeof operator.
For example, you can check whether the revenue parameter is a
number first:

function calculateProfit(revenue: number | string){


// Narrow the type down to number
if(typeof revenue === 'number'){
revenue.toFixed();
}
// It's not a number, so it must be a string
else {
revenue.endsWith('a');
}
}

When the type of the revenue parameter is not a number,


TypeScript smartly assumes that the type is a string, and so you
get the all string methods and properties available in the else
block.
Intersection Type
While union type can be either one of the specified types, an
intersection type must be a combination of all the types
specified.

To show you an example, let’s create two separate types named


Person and Contact as follows:

type Person = {
name: string
};

type Contact = {
email: string
};

To create an intersection type, you need to use the ampersand &


operator.

Let’s create a new type from the intersection of Person and


Contact types:

type Associate = Person & Contact;

The Associate type is created from the combination of Person


and Contact types.

This means the variable of type Associate needs to have all of


Person and Contact properties:

const myAssociate: Associate = {


name: 'Jane',
email: '[email protected]'
}

The Associate type must follow the specifications defined in the


intersection types. You can still define optional properties if you
want.

And that’s how you create an intersection type. Onward to


literal types!

Literal Type
The literal type is used to narrow the value that can be put in a
variable or function parameter.

So far, you’ve used common types such as string and number to


annotate your variables.

Using literal types, you can narrow down the value that can be
specified. For example, a number variable must either be a 10
or 100:

const myNumber: 20 | 30 = 20;

Now myNumber variable must either be 20 or 30. You can’t put


any other value.

The same can be done to a string type as follows:

// String: must be English or Spanish


const language: 'English' | 'Spanish' = 'English';

Literal types enable you to allow only exact values and nothing
else.
The Unknown Type
The unknown type is very similar to any type.

The only difference is that you’re not allowed to do anything


with the unknown type unless you narrow this type.

For example, suppose you have a function that accepts an any


parameter as follows:

function process(something: any){


something.toUpperCase();
something.toFixed();
}

Because something is an any type, TypeScript won’t do any check


on that parameter, so any method you call is treated as valid.

Now if you change the type to unknown, you’ll get errors:

function process(something: unknown){


something.toUpperCase();
~~~~~~~~~
something.toFixed();
~~~~~~~~~
}

An unknown type is used to prevent unsafe access to a parameter.

To access an unknown type, you need to narrow down the type


first. For example:

function process(something: unknown){


if (typeof something === 'string'){
something.toUpperCase();
}
if (typeof something === 'number'){
something.toFixed();
}
}

The unknown type is like a safe guard that forces you to check on
the parameter before doing anything that may cause an error.

The Never Type


The never type is used to mark a function that never returns
any value.

For example, suppose you have a function that’s used to throw


an error and stop code execution as follows:

function reject(message: string) :never {


throw new Error(message);
}

// A random error message just for demo


reject('Fail to execute');
console.log('Process done');

In the code above, the reject() function will throw an error no


matter what.

This means it stops the running program and never returns a


value, so the never type is perfect for it.

Now if you look in your editor, you’ll see that the console.log()
call below the function call is marked as unreachable code:
This warning means that code line will never be executed.

TypeScript gives you the option to allow or disallow


unreachable code through the allowUnreachableCode option in
your tsconfig.json file.

If you specify the option as false like this:

"allowUnreachableCode": false

Then the unreachable code becomes an error instead of a


warning.

You can combine this option with "noEmitOnError": true to stop


the compilation process in case of unreachable code.

The Satisfies Operator


The satisfies operator is used to check whether a certain object
fulfills the minimum requirement of a specific type.

Let me show you an example, suppose create a custom type for


the Conference object as follows:

type Conference = {
title: string
schedule: string | Date
description: string
};

Next, we create an object of the Conference type as follows:

const conference: Conference = {


title: 'TS Conference',
schedule: new Date('2024-04-10'),
description: 'The Annual TypeScript Conference is here!'
};

Now if you try to access a method of the schedule property such


as getDay(), you’ll get an error:

conference.schedule.getDay()
~~~~~~

As we’ve learned in the previous section, a union type must be


narrowed down before you can access the properties and
methods of that type.

One way you can solve this problem is by using an if statement:

const conference: Conference = {


title: 'TS Conference',
schedule: new Date('2024-04-10'),
description: 'The Annual TypeScript Conference is here!'
}

if(conference.schedule instanceof Date){


conference.schedule.getDay(); // OK
}

But a better way is to use the satisfies operator.


You need to add the satisfies operator after the object
initialization as follows:

const conference = {
title: 'TS Conference',
schedule: new Date('2024-04-10'),
description: 'The Annual TypeScript Conference is here!'
} satisfies Conference

The satisfies operator validates that the conference object


fulfills the requirements defined in the Conference type.

TypeScript looks at the schedule property value and recognizes


that it follows the defined custom type, which is a Date object.

That’s why you don’t need to check the value after initialization
anymore.

Summary
TypeScript allows you to combine types using the union and
intersection types. You can narrow a type down using an if
statement or the satisfies operator.

You’ve also learned how to narrow down a type using a literal


type, and discover two advanced types provided by TypeScript,
which are the unknown and never types.
CHAPTER 7: ADVANCED OBJECT
TYPING

Now that you know advanced types such as union and literal
types, it’s time to explore TypeScript features that can help
when typing objects.

TypeScript also adds two new object types called enum and
tuple that we’re going to explore together in this chapter.

The 'keyof' Operator


The keyof type operator is used to take the keys of an object and
make them literal types.

When you have only one key, you’ll get a string literal. When
you have more than one, you’ll get a union of string literals:

type Person = {
name: string;
age: number;
};

type Keys = keyof Person;


// type Keys = "name" | "age"
The keyof property is used only when you need to perform
operations that involve object keys, usually some sort of search
and retrieve from a specific object.

For example, suppose you have a function that searches


through the Person type as follows:

type Person = {
name: string;
age: number;
};

const myProfile: Person = {


name: 'Nathan',
age: 28
}

function getValue(key: string, obj: Person){


return obj[key];
}

const age = getValue('age', myProfile)

In the code above, the getValue() function is used to retrieve


the value of a specific key from an obj object of type Person.

The code above looks fine, but there’s actually an error:

function getValue(key: string, obj: Person){


return obj[key];
~~~~~~~~
}

Because we annotate the key parameter as a string, TypeScript


doesn’t know what value will be returned by the return
statement.
This implicitly annotates the obj[key] to be of any type, and you
can verify this by hovering over the age constant:

An object key is limited, so a variable used as a key must always


be literally typed.

One way to fix the error is to create a literal union of the object
keys:

function getValue(key: 'name' | 'age', obj: Person) {


return obj[key];
}

When you pass a literal union as shown above, TypeScript then


knows that the function always returns either a string or a
number because that’s all the Person type has.

Hover over the age constant, and you’ll see the annotation
changed:
const age: string | number

But if you have many keys, it’s very cumbersome to type them
all.

This is when you use the keyof operator to help you out:

function getValue(key: keyof Person, obj: Person) {


return obj[key];
}

And that’s all there is for the keyof operator.

Type Mapping
Type mapping is a way to reuse and customize an existing type
to create a new type.

For example, suppose you have a Person type as follows:

type Person = {
name: string;
age: number;
};

After a while, you decided you need to create a Person object


that has readonly properties:

type LockedPerson = {
readonly name: string;
readonly age: number;
};

As you can see, the LockedPerson type is quite repetitive here.


Instead of typing the properties again, you can map the types
from the Person object as follows:

type LockedPerson = {
readonly [K in keyof Person]: Person[K]
};

To map the keys, you need to use the index signature and the
keyof operator together as in [K in keyof Person].

To fill the values of the property, you simply access the value of
the type as in Person[K].

You can also modify the values of the type if that’s what you
want.

Imagine you have a Feature type that can enable dark mode and
new layout as follows:

type Feature = {
darkMode: string;
newLayout: string;
};

You can change the value from string to boolean using type
mapping:

type Feature = {
darkMode: string;
newLayout: string;
};

type FeatureOptions = {
[K in keyof Feature]: boolean
};
The FeatureOptions will have boolean properties as shown
below:

type FeatureOptions = {
darkMode: boolean;
newLayout: boolean;
};

And that’s how you use type mapping in practice.

Tuples
A tuple is an array object with an ordered sequence of types.

When you create a tuple, the types of each element represented


in the array are annotated on the declaration.

The code below shows how you can create a tuple:

const myTuple :[number, string] = [1, 'Nathan'];

A tuple must have a fixed number of elements. If you annotate


two elements, then the array needs to have two elements.

Adding or subtracting the number of elements will cause an


error.

Enums
An enum is a set of constant values grouped together. You can
access an enum value similar to how you access an object’s
property.
For example, suppose you have a constant that indicates the
size of an item:

enum Sizes { Small, Medium, Large }

console.log(Sizes.Small); // 0
console.log(Sizes.Medium); // 1
console.log(Sizes.Large); // 2

Enums are used to create a set of constant values that you need
to refer to in your code.

By default, enums have number values starting from 0. You can


change the order of the numbers by specifying only the first
constant value like this:

enum Sizes { Small = 3, Medium, Large }

console.log(Sizes.Small); // 3
console.log(Sizes.Medium); // 4
console.log(Sizes.Large); // 5

The following constants will continue from the value you


specify on the previous constant.

You can also define custom values for each constant like this:

enum Sizes { Small = 's', Medium = 'm', Large = 'l' }

console.log(Sizes.Small); // s
console.log(Sizes.Medium); // m
console.log(Sizes.Large); // l

When you use a string value, you need to initialize all enum
members as shown above.
Summary
The keyof operator allows you to create a literal union of
property keys defined in a custom object type.

By combining the keyof operator with index signature, you can


map the types of a custom object and customize them without
repeating yourself.

Tuples and enums are advanced TypeScript types that you can
use for specific use cases.

You might not need them at all when developing web


applications, but you need to know in case you meet them in
your development journey.

In the next chapter, you’re going to learn how to type classes.


CHAPTER 8: TYPING CLASSES
AND OBJECT-ORIENTED
PROGRAMMING

As you write a program using JavaScript, there will be times


when you need to create objects that have similar properties.

For example, suppose you have two objects called car and
bicycle as follows:

const car = {
color: 'silver',
wheels: 4,
};

const bicycle = {
color: 'red',
wheels: 2,
};

Here, you can see how these two objects have similar
properties: color and wheels.

Instead of initializing the two objects from scratch, you can


generate them from a class.
A class is a blueprint that describes the general properties and
methods that an object can have. You can use a class to generate
multiple objects of the same type.

A class would resemble real-world things and definitions. For


example, a Vehicle class can represent many machines that
transport people and goods.

Objects you can create from the Vehicle class might be a car, a
truck, or a bicycle.

Note that while a real car or bicycle would have more


properties than just colors and wheels, we’re going to simplify
the examples for learning purposes.

How to Create a Class


To create a class, you need to write the class keyword, followed
by the class name and curly brackets. For example:

class Vehicle {}

Like custom types before, the class name uses the PascalCase
format.

Now that you have a class, let’s start adding properties for this
class.

Following the objects we created before, the class should have


the color and wheels properties:

class Vehicle {
color: string
wheels: number
}

After defining the properties, you need to write the


constructor() method of the class.

The constructor() method is a special method that gets called


automatically when you create an object from the class.

Inside the method, you should initialize all properties required


for the object by using the this.propertyName syntax:

class Vehicle {
color: string
wheels: number

constructor(color: string, wheels: number){


this.color = color;
this.wheels = wheels;
}
}

The this keyword refers to the object created from the class.

To create an object from the class, you need to specify the new
keyword followed by a call to the class name as follows:

const car = new Vehicle('silver', 4);

console.log(car.color); // silver
console.log(car.wheels); // 4

const bicycle = new Vehicle('red', 2);

console.log(bicycle.color); // red
console.log(bicycle.wheels); // 2
Here, both car and bicycle are objects created from the Vehicle
class.

The arguments you specify when calling the class are passed to
the constructor() method, giving values to properties defined in
the class.

Next, let’s see how you can write class methods.

Defining Class Methods


A method is a function defined inside a class or an object. When
you define a method in a class, that method will be available for
all objects created from that class.

Below the cosntructor() method, let’s add the warning() and


drive() methods to the Vehicle class:

class Vehicle {
// constructor ...

warning(): void {
console.log('Honk!!');
}

drive(speed: number): void {


console.log(`Driving at ${speed} mph`);
}
}

When adding a method, you don’t need to write the function


keyword before the method name.

To call these methods, you need to use the


objectName.methodName() syntax as follows:
const myCar = new Vehicle('silver', 4);

myCar.warning();
myCar.drive(40);

Output:

Honk!!
Driving at 40 mph

Notice how you can pass an argument to the drive() method


just like any other function.

Why a Function in Object/Class Is


Called a Method?
You might wonder, if a method is just a function in a class or an
object, then why the additional term? It seems unnecessary.

It’s because the term method instantly implies that there’s a


class that this function belongs to.

When you call a global function such as parseInt(), you can call
the function without specifying the object the function belongs
to as follows:

parseInt('100');

But when you’re calling a method, you need to specify the


object from which you call the function, followed by a dot .
notation.

For example, here’s how you call a string object method:


const firstName = 'Nathan';

userName = firstName.toUpperCase();

The term method gives more information to you as the person


who works with the program, allowing you to differentiate
between "functions" and "functions in an object" without
having to see the source code.

The same also applies for properties. The term is used to


differentiate between "variables" and "variables in an object".

Inheritance
Inheritance is a feature that allows you to create a new class
that extends another class, enabling you to reuse code written
in another class.

For example, we’ve already defined a class named Vehicle


before. As we know, a vehicle might be many things: a car, a
ship, a truck, or even a rocket.

All these vehicles can have their own specific traits and
behaviors. For example, cars have four wheels and a specific
transmission setup.

Here’s an example of a Car class that you might write:

class Car {
color: string
wheels: number
name: string
transmission: string

constructor(color: string, wheels: number, name: string,


transmission: string) {
this.color = color;
this.wheels = wheels;
this.name = name;
this.transmission = transmission;
}

start(): void {
console.log('Starting the engine..');
console.log(`${this.name} is online`);
}

warning(): void {
console.log('Honk!!');
}

drive(speed: number) :void {


console.log(`Driving at ${speed} mph`);
}
}

Now notice how many chunks of code above are repeated from
the Vehicle class.

In the Vehicle class, we’ve already defined these properties and


methods:

1. color
2. wheels
3. warning()
4. drive()

Instead of writing the Car class from scratch, you can make Car
a subclass of the Vehicle class, which allows you to reuse the
properties and methods of the Vehicle class on the Car class.

The new class that inherits from a previous class is called a


subclass or child class, while the previous class is called a
superclass or a parent class

To create a subclass, you need to use the extends keyword and


specify the name of the parent class after that:

class Car extends Vehicle {}

Here, we create a Car child class from the Vehicle parent class.

Next, you need to define the properties and the constructor()


method in this Car class:

class Car extends Vehicle {


name: string
transmission: string

constructor(color: string, wheels: number, name: string,


transmission: string){
super(color, wheels);
this.name = name;
this.transmission = transmission;
}
}

The super() method refers to the constructor() method of the


parent class. In the code above, it refers to the Vehicle class.

By calling super(), we called the constructor() method of the


Vehicle class so that we can initialize the color and wheels
properties there.
This way, we only need to initialize properties exclusive to the
Car class in the constructor() method.

Next, write the start() method in the Car class as follows:

class Car extends Vehicle {


// constructor ...

start(): void {
console.log('Starting the engine..');
console.log(`${this.name} is online`);
}
}

Alright, now you have a working child class. Let’s initialize an


object from that class.

When you create an object from a child class, that object also
has the properties and methods of the parent class:

const myCar = new Car('silver', 4, 'Creta', 'Automatic');

myCar.start();
myCar.drive(40);
myCar.warning();

Output:

Starting the engine..


Creta is online
Driving at 40 mph
Honk!!

Isn’t that amazing? You can reuse the properties and methods
of the parent class through the child class as if they are one.
The reverse is not true, though. Any properties and methods
you defined in the child class won’t be accessible from the
parent class.

The parent class actually knows nothing about the child class.

Overriding methods
Beyond just passively inheriting properties and methods of a
parent class, a child class can also override inherited methods.

Overriding a method simply means replacing the method a


child class inherits.

This is done so that the method does a different thing when


called from the child class.

To override a method, you need to define the same method in


the child class, add the override modifier, and then give it a
different instruction.

Going back to the Car class, let’s try to override the warning()
method:

class Car extends Vehicle {


// Other code ...

start(): void {
console.log('Starting the engine..');
console.log(`${this.name} is online`);
}

override warning(): void {


console.log('Badum Tis!');
}
}

Next, you can test the method by creating an object from both
classes:

const myCar = new Car('silver', 4, 'Creta', 'Automatic');


const bicycle = new Vehicle('red', 2);

bicycle.warning(); // Honk!!
myCar.warning(); // Badum Tis!

As you can see, the warning() method from the Vehicle class
does a different thing from the warning() method in the Car
class.

Depending on the given requirements, you can override as


many or as few methods from the parent class as you need.

Now the code above still works if you remove the override
modifier. This is because the keyword is optional by default.

To make the override keyword mandatory, you can enable the


noImplicitOverride option in tsconfig.json like this:

"noImplicitOverride": true

Now TypeScript will complain when you override a method


without adding the override modifier.
Object-Oriented Programming
Paradigm
The use of classes allows you to write a program in the object-
oriented style. This is known as object-oriented programming,
or OOP for short.

Classes model real world things and definitions, and by creating


objects from the classes, you can organize the objects to work
together for a specific purpose.

Each object can have attributes (data) and methods (functions)


that operate on the data.
A program created using the object-oriented approach usually
creates many objects out of many classes, and these objects
would work together to achieve a specific goal.

Summary
In this lesson, you’ve seen how classes allow you to define the
traits and behaviors of a real world object. It allows you to
define the abilities and limitations of the object you later
initialize from that class.

You’ve also learned how the inheritance feature allows you to


create a child class that extends the capability of the original
class.

Through inheritance, a child class can use the same traits and
behaviors owned by the original class and extend them.

Classes enable developers to code in object-oriented style.

Some people love object-oriented programming, while others


prefer functional programming, which uses functions instead of
objects as the building block of the program.

But honestly, object-oriented or functional are just two different


ways to solve the same problem: making a software that’s
useful for others.

Objects and functions have their strengths and weaknesses.


Instead of debating which one is the best coding style, just use
them when the requirement calls for it.
CHAPTER 9: ADVANCED CLASS
FEATURES (PART 1)

TypeScript provides many useful features to control and


manipulate the abilities of a class that doesn’t exist in
JavaScript.

We’re going to explore them together in this chapter.

Value Modifiers
Value modifiers are keywords that you can use to control what
can be done to the properties of the class.

You already learned them from the previous chapters: they are
the readonly and optional (?) modifiers.

For example, suppose you have a User class with the id, name,
and picture properties.

You can make the id property readonly so that it can’t be


changed after initialization:
class User {
readonly id: number
name: string
picture: string

constructor(id :number, name :string, picture: string){


this.id = id;
this.name = name;
this.picture = picture
}
}

This way, the id property can only be assigned when you create
an object from the class, when the constructor() method is
executed.

Next, the picture property can be made optional as follows:

class User {
readonly id: number
name: string
picture?: string

// constructor ...
}

When you define a property as optional, you can choose


whether to initialize it in the constructor() or not.

If you initialize the optional property, then you can make the
constructor parameter that corresponds to the property
optional as well:

class User {
readonly id: number
name: string
picture?: string
constructor(id :number, name :string, picture? :string){
this.id = id;
this.name = name;
if(picture){
this.picture = picture;
}
}
}

// Initialize all parameters


const user = new User(1, 'Nathan', 'image.png');

If you choose not to initialize the property, you can omit the
parameter from the constructor() like this:

class User {
readonly id: number
name: string
picture?: string

constructor(id :number, name :string){


this.id = id;
this.name = name;
}
}

But this means you can’t initialize the picture property when
you instantiate (create) the object.

You can only assign the property a value after creating it:

class User {
readonly id: number
name: string
picture?: string

constructor(id :number, name :string){


this.id = id;
this.name = name;
}
}

// Cannot initialize optional parameter


const user = new User(1, 'Nathan');
// Assign a value to the optional property
user.picture = 'image.png';

It’s up to you which constructor() signature you want to use.

Visibility (or Access) Modifiers


The visibility modifiers are used to determine how the
properties and methods of a class can be accessed.

These modifiers give more control over how the properties and
methods of a class should be used.

There are 3 visibility modifiers provided by TypeScript: public,


private, and protected.

Let’s see how they work with examples.

The Public Modifier


The public modifier is the default access control granted to
properties and methods of a class.

When a property is public, you can access the property from


the object directly:

class User {
public name: string

constructor(name :string){
this.name = name;
}
}

When you create a public property or method, you can omit the
modifier just fine:

class User {
name: string // implicitly public

constructor(name :string){
this.name = name;
}
}

The Private Modifier


The private modifier is used to make properties and methods
accessible only from the class itself.

For example, you can make the name property private like this:

class User {
private _name: string

constructor(name :string){
this._name = name;
}
}

When creating a private property, you need to add an


underscore _ in front of the property name.

There’s nothing wrong if you don’t add it. It’s just a convention
in TypeScript programming.

Because the _name property can’t be accessed directly, you need


to create a method that retrieves that property as follows:
class User {
// ...

getName() {
return this._name;
}
}

// Create an object of the User class


const user = new User('Nathan');

// Get the name value using a function


user.getName();

If you try to access the user._name property, you’ll get an error.

The Protected Modifier


The protected modifier makes a property or a class accessible
from the class and all its child classes, but not from outside:

class User {
// Create a protected property
protected name: string

constructor(name :string){
this.name = name;
}
}

// Extend the User class


class Account extends User {
email: string

constructor(name: string, email: string){


super(name);
this.email = email;
}

// Access the user property


getUsername(){
return this.name;
}
}

In the code above the getUsername() method of the Account class


can access the this.name property because it’s protected.

If you change the name modifier to private, then accessing the


property from the Account class will cause an error.

Protected properties are inherited, but private properties are


not.

When to Use These Modifiers?


The public modifier is the default, so you will use it most often.

The private modifier is used when you don’t want a property or


method to be public.

The protected modifier is the one rarely used, because it’s only
for advanced use cases: you want to allow child classes to have
access to the property or method, but you don’t want the public
to have access.

The public and private modifiers are usually enough. Only use
protected when you have a strong reason to.

Parameter Properties Shorthand


The parameter properties is a handy feature in TypeScript that
allows you to create a cleaner and simpler class.
So far, the class we have written is very verbose. We need to
define the property, create a constructor method with
parameters, then assign those parameters to class properties:

class User {
readonly id: number
name: string
private _age: number

constructor(id :number, name :string, _age: number){


this.id = id;
this.name = name;
this._age = _age;
}
}

Using parameter properties, you can turn the constructor


parameters into class properties of the same name.

The class above can be reduced to this:

class User {
constructor(
readonly id: number,
public name: string,
private _age: number) {}
}

To turn a parameter into a property, you need to define the


visibility modifier in front of the parameter name.

Now you can create an object just as before:

const user = new User(1, 'Nathan', 28);

// try to get age, which is private:


console.log(user._age);
~~~~
With parameter properties, you don’t need to define the class
properties and the constructor() body.

Getters and Setters


When you set a property as private, then that property is not
accessible outside of the class:

class User {
constructor(
readonly id: number,
public name: string,
private _age: number) {}
}

const user = new User(1, 'Nathan', 28);

// Getting age property causes an error


console.log(user._age);
~~~~

To enable access to private property from outside of the class,


you can create the getter and setter methods inside the class.

Here’s an example of getter and setter methods for the age


property:

class User {
constructor(
readonly id: number,
public name: string,
private _age: number) {}

get age() {
return this._age;
}
set age(age) {
this._age = age
}
}

To create a getter method, you need to add the get modifier in


front of the method. The same applies for a setter.

Now you can access and assign a new value to the age property
as if it’s a public property:

const user = new User(1, 'Nathan', 28);

// Set the age property


user.age = 30;

// Get the age property


console.log(user.age);

If you want to make a property private and read only, you don’t
need to define the setter method.

Summary
There are several more class features provided by TypeScript,
but let’s take a short break here to recap what we’ve learned.

In this chapter, we’ve seen how class properties can be turned


read only or optional.

We’ve also learned about visibility modifiers, which control the


access to properties and methods of a class.

After that, we learned how to use the parameter properties


shorthand to make the class definition simpler and less verbose.
Finally, we see how to create getter and setter methods to access
private properties.
CHAPTER 10: ADVANCED CLASS
FEATURES (PART 2)

This chapter still covers the class features added by TypeScript.

You will see how to make static properties and methods, create
abstract classes, and define the shape of an object using an
interface.

Let’s jump in and get started!

Static Modifier
The static modifier is used to create properties and methods
that belong to the class instead of the object created from that
class.

To show you an example, suppose you create a class that


represents a square.

A square will always have 4 sides, so you create the sides


property and initialize it as follows:
class Square {
sides: number = 4;
}

Next, you create a calculatePerimeter() static method as shown


below:

class Square {
static sides: number = 4;

static calculatePerimeter(side: number){


return Square.sides * side;
}
}

Note that you need to define the class name when accessing the
sides property as in Square.sides.

Next, you can try to call the method directly:

class Square {
static sides: number = 4;

static calculatePerimeter(side :number){


return Square.sides * side;
}
}

const perimeter = Square.calculatePerimeter(5);

console.log(perimeter); // 20

As you can see, adding the static modifier allows you to access
the calculatePerimeter() method directly.

While you can have both static and non-static members


(properties and methods) in a single class, it’s better to make the
class properties and methods identical.

This means that when you have one static member, consider
the whole class as static and don’t add a non-static member to
it.

The Abstract Modifier


The classes, methods, and properties you create in TypeScript
can be modified as abstract.

Similar to type alias, abstract is used to define the specification


of methods and properties. For example:

abstract class Shape {


constructor(public size: number) {}
// Abstract method
abstract getArea(): number;
}

You can’t create an object from an abstract class.

You need to extend the abstract class in a regular class as shown


below:

class Square extends Shape {


constructor(size: number){
super(size)
}

// Implement the abstract method


getArea() :number {
return this.size ** 2;
}
}
Using the extends keyword, the Square class is now a child of the
Shape class, and it must implement all abstract members
defined in the Shape class.

We only have one abstract method in the example above, but


you can define as many abstract methods as you need.

An abstract class is like a blueprint for classes. Any class that


wants to extend an abstract class must implement the
requirements defined in that class.

Interfaces
An interface is used to define the shape of a class, much like the
abstract class.

You can create an interface by using the interface keyword as


follows:

interface Shape {
size: number
getArea(): number
}

To use an interface on a class, you need to add the implements


keyword between the class name and the interface name as
follows:

class Square implements Shape {


constructor(public size: number) {}

getArea() {
return this.size ** 2;
}
}

Notice that we are not extending the interface here. We just


implement the structure defined in the interface.

A class that extends an abstract class becomes a child of the


abstract class.

But a class that implements an interface is not a child of the


interface.

You can also use an interface directly on an object like this:

const square :Shape = {


size: 5,
getArea: function () {
return this.size ** 2;
}
}

But you can’t use an abstract class on an object.

Interface vs Type - Which One to Use?


Aside from interface, you can also implement a type alias to a
class as follows:

type Shape = {
size: number
getArea(): number
}

class Square implements Shape {


constructor(public size: number) {}

getArea() {
return this.size ** 2;
}
}

An interface can only be used to define an object, while type


can be used for virtually any type:

type Person = string | boolean // OK

// interface Person = string | boolean // NOT OK

So which one to use? It depends on what you want to do with


the type you are creating.

If you want to create a custom type that’s not an object, you


need to use type as shown above.

When you want to type an object, using an interface can be


clearer and shorter. Another plus point is that people who code
in other static type languages should be familiar with this
feature.

TypeScript adds the interface feature to conform to the object-


oriented programming principle, while type is a unique feature
of TypeScript.

Summary
You just finished exploring classes in TypeScript.
Congratulations!

Classes are the most complex entity in the object-oriented


programming world because they are so powerful.
They can be used to create object instances, define child classes,
and guard properties and methods using modifiers.

You can even make class members static, so you don’t need to
create an object to access them.

But as told in Spider-Man, with great power comes great


responsibility.

Classes are so powerful that it’s easy for developers to create a


highly complex solution when simpler ones would be enough.

Next, we will explore one of the most useful TypeScript features


that is simpler than classes.
CHAPTER 11: DYNAMIC TYPING
WITH GENERICS

So far, the types we annotate to our functions, classes, objects,


and other entities are all static.

For example, suppose you have a function that converts its


parameter into an array as follows:

function createArray(value: string){


return [value];
}

const users = createArray('Nathan');

console.log(users); // ['Nathan']

With the implementation above, the type of the value


parameter is static.

When we want to pass a number to the function, we get an


error:

function createArray(value: string){


return [value];
}
const users = createArray(1); // ERROR

One way to solve this is to add the number type to the key
parameter:

function createArray(value: string | number) {}

But this is also not a complete solution. What if later we want to


allow the boolean type for the value parameter?

We need to change the function definition again:

function createArray(
value: string | number | boolean
) {}

Maybe we can annotate the value parameter as any, but then


again any is not recommended for use, and we don’t get code
completion from TypeScript.

The best solution to this problem is to use Generics.

Generics are simply type parameters that you can define in


entities. When you call an entity with a generic, you then pass
the argument for the type.

Generic Functions
When used in functions, generics enable you to define the type
for parameter(s) and return values of the function when you
call the function, not when you define the function.
Generics are specified using angle brackets <> next to the
function name as follows:

function createArray<T>(value: T) {
return [value];
}

A generic is usually written as T, which is short for 'Type'.

When you call the createArray() function, you need to pass the
value for T as follows:

createArray<string>('Nathan'); // ['Nathan']

createArray<number>(100); // [100]

Generics also have type inference enabled. So T can be inferred


from the argument type if you omit it:

function createArray<T>(value: T) {
return [value];
}

const users = createArray('Nathan');

const scores = createArray(100);

In the code above, users will be a string array, and scores will
be a numbers array.

You can also type the return value of the function by specifying
T after the parameters definition:

function createArray<T>(value: T) :T[] {


return [value];
}
Because we return an array, we need to define the return value
type as T[].

If we return a regular value, then just T will be enough:

function createValue<T>(value: T) :T {
// return just the value
return value;
}

When you use generics as shown above, TypeScript will use the
T value to offer you the auto complete feature.

If you define T as a string type, then TypeScript will show all


properties and methods of the string object.

Generic Types
Aside from functions, generics can also be used in custom types
created using the type keyword.

This feature is commonly used when you type the response


object from API endpoints.

For example, suppose you are building a web application where


you fetch data from different endpoints.

You can first create the type for the API response as follows:

type Result<T> = {
data: T | null
error: string | null;
}
Next, you define the types that will be used for the data object.

The next two types are simplified examples of response that


you can receive:

type User = {
name: string
}

type Product = {
price: number
}

Next, you create a function called getData which accepts a T


type and pass it to Result as follows:

function getData<T>(url: string): Result<T>{


// Fill the response manually for illustration
return {
data: null,
error: null
}
}

In the code above, the <T> passed to getData() will be forwarded


to Result<T>.

Now you can pass either User or Product type when calling the
getData() function, and TypeScript will know what auto
complete feature to offer you then:

const user = getData<User>('url');

user.data?.name; // data is <User>

const product = getData<Product>('url');


product.data?.price; // data is <Product>

And that’s how you can create generic types for typing API
response objects.

Generic Classes
Generics are used in classes so that objects created from the
class can have dynamic properties.

For example, suppose you have a class that creates a key-value


pair as follows:

class KeyValuePair {
constructor(public key: string, public value: string) {}
}

const pairOne = new KeyValuePair('name', 'Nathan');

On the code above, the key and value properties are statically
defined as a string type.

To make the properties generic, add a <T> after the class name,
and use it when defining the parameters of the constructor
class:

class KeyValuePair<T> {
constructor(public key: T, public value: T) {}
}

Now the type of key and value can be passed when you create a
new instance from the class:
class KeyValuePair<T> {
constructor(public key: T, public value: T) {}
}

const pairOne = new KeyValuePair<string>('name', 'Nathan');

const pairTwo = new KeyValuePair<number>(1, 2);

While the type of the properties above are now generics, there’s
still a problem here.

Notice that the key and value properties must be the same type
because we use T on both.

You can assign different types to the properties by adding more


than one generic parameter.

For example, add a <T, U> parameter like this:

class KeyValuePair<T, U> {


constructor(public key: T, public value: U) {}
}

This way, the T type is used to annotate the key property, while
the U type is used for the value property.

The letter U is used here because that’s the next letter after T in
the alphabetical sequence.

In reality, you can use any name you want for generic
parameters, just like function parameters. The examples below
are all valid:

class KeyValuePair<Tkey, Tvalue> {


constructor(public key: Tkey, public value: Tvalue) {}
}
class KeyValuePair<A, B> {
constructor(public key: A, public value: B) {}
}

class KeyValuePair<Type1, Type2> {


constructor(public key: Type1, public value: Type2) {}
}

Any kind of styling works for generics, but make sure you use
only one style for naming generics to avoid confusion.

Now you can define the type for key and value separately when
creating an instance:

const pairOne = new KeyValuePair<string, number>('age', 27);

const pairTwo = new KeyValuePair<number, string>(1, 'JavaScript');

You can also create generic methods inside a class.

For example, the following static method is a generic:

class ArrayHelper {
static createArray<T>(value: T) {
return [value];
}
}

When calling the method, you can specify the type as shown
below:

ArrayHelper.createArray<string>('Nathan');
ArrayHelper.createArray<number>(5);

And that’s how you use generics in classes.


Generic Conclusion
Generics is a powerful feature that you can use to parameterize
types.

Instead of adding the types in the definition, you add a


placeholder <T>, which you can use in many places: function
parameters and return values, custom type properties, class
properties, and methods.

If this is your first time learning about generics, the syntax


might be confusing to you.

But don’t worry because when developing web apps, you are
more likely to use generics rather than making your own.

For example, suppose you are trying to get the value of an


HTML <input> element as follows:

const inputElement = document.querySelector('#input');


inputElement?.value;
~~~~~

In the code above, there’s a TypeScript error because the


property value doesn’t exist on type Element.

When you hover over the querySelector method, you’ll see the
call to that method actually passes a generic <Element> interface
as follows:
The Element interface is the base type used by all HTML
elements. It only has methods and properties common to all
elements.

Because we want to select an <input> element, we can tell


TypeScript about our intent using generic as follows:

const inputElement = document.querySelector<HTMLInputElement>


('#input');
inputElement?.value;

The HTMLInputElement is the TypeScript interface that extends


the HTMLElement interface, which extends the Element interface.

As you can see, you’re more likely to specify a type or interface


to pass as a generic than creating a generic entity.

You use generics only when you see errors as shown above:
when TypeScript complains that the variable, object, or
function you use doesn’t have certain properties.

You’ll learn more about this later when integrating TypeScript


into libraries and frameworks.
Summary
In this chapter, you’ve learned how to use generics to
parameterize your types.

By using generics, you essentially create placeholder types for


entities that can be filled when you use that entity.

You can define the function types when calling the function, or
you can define the class types when creating an instance of that
class.

When developing web applications, you’re more likely to use


generics instead of creating one from scratch.
CHAPTER 12: MODULES, TYPE
ASSERTION, AND
DISCRIMINATING UNIONS

You’ve learned almost all of the important TypeScript features


needed for application development.

There are a few more features we need to explore before


learning how to integrate TypeScript into real world projects:

1. Modules in TypeScript
2. Type assertion with the as operator
3. Discriminating unions

Let’s jump in right away.

Modules in TypeScript
TypeScript supports all JavaScript export/import syntax for
creating modular code.
When using TypeScript, you can use the ES Module (ESM)
syntax to export and import code as follows:

// Export a function
export function sum(x: number, y: number){
return x + y;
}

// Import the function


import { sum } from "./helper";
sum(1, 2);

You can configure the TypeScript compiler to adjust the


export/import syntax by setting the module option.

By default, the tsconfig.json file uses the 'commonjs' format:

"module": "commonjs",

The 'commonjs' format uses the module.exports and require


syntax as follows:

// Export the function


function sum(x, y) {
return x + y;
}
exports.sum = sum;

// Import the function


const { sum } = require("./helper");
sum(1, 2)

The good thing about TypeScript is that you can select the
module format you want to use for the generated code.

The module option is set to 'commonjs' so that you can run the
generated code using Node.js without further configuration.
If you want to use the ES Module format, you can change the
module option as follows:

"module": "ES2015"

This way, the generated code will use the export/import syntax,
but you can’t run the code in Node.js without setting the "type":
"module" option in the package.json file.

{
"type": "module" // Add this in package.json to run ESM syntax
}

Personally, I leave the module option in tsconfig.json set to


'commonjs' unless I know for sure the project won’t be executed
using Node.js.

Type Assertion Using the 'as' Operator


The as operator is used to assert the type of a variable.

This operator is commonly used to tell TypeScript that you


know the type of the variable with certainty.

For example, suppose you use the querySelector() method to


get an input element as follows:

const inputElement = document.querySelector('#input');

console.log(inputElement.value);

The querySelector() method returns either an Element or a null.


Because the value property doesn’t exist in the Element type, the
code above causes an error.

To solve this problem, you can use the as operator to assert the
value type:

const inputElement = document.querySelector('#input') as


HTMLInputElement;

console.log(inputElement.value);

Wait a minute, isn’t this similar to when we apply a generic


type to the querySelector() method?

Well, they look similar at first, but generics and type assertions
work differently under the hood.

A generic requires the method or class to accept a generic type


<T>. It doesn’t work when the method doesn’t accept a generic.

The purpose of generics is to make entities such as functions,


classes, and types reusable.

On the other hand, type assertion can be used on all kinds of


variables and return values.

The purpose of type assertion is to tell TypeScript that you


know the type of a variable for certain.

If you hover over the inputElement above, you’ll see that the
type becomes HTMLInputElement and nothing else:

const inputElement: HTMLInputElement


Now if you use the generic type, the result is a union between
HTMLInputElement and null:

const inputElement: HTMLInputElement | null

How a generic type is used is described in the method itself,


while the as operator always asserts that the variable is exactly
a specific type.

This operator is commonly used when you meet a variable of


any type, such as when receiving a response from the fetch API
call:

fetch('https://abc.com')
.then(response => response.json())
.then(data => {
// ...
});

After calling the response.json() method as shown above,


TypeScript automatically annotates the data variable as an any
type.

Now if you know for certain the shape of the returned data, you
can create a type for that data, then call the as operator to
assert the data variable like this:

type Task = {
id: number;
title: string;
};

fetch('https://abc.com')
.then(response => response.json())
.then(data => {
(data as Task).title;
});

After you assert the data variable, TypeScript can offer you auto
complete as follows:

Type assertion is really useful when you encounter an any type,


but you actually know the exact type for that data.

If you don’t know the type of the data for certain, then it’s
better to use type narrowing.

Discriminating Unions
Discriminating unions is a feature that allows TypeScript to
narrow down the current type by using one specific field as an
indicator.

The best way to understand this feature is by using an example,


so suppose you have an ApiResponse type as shown below:

type ApiResponse = {
state: 'success' | 'failed';
data?: { title: string };
error?: { message: string };
};
The ApiResponse type above is used to type an object received
after sending a network request.

When the state is 'success', the data property will be populated,


when the state is 'failed', the errorMessage property will be
populated.

Now if you use an if statement to handle the response like this:

function handleResponse(res: ApiResponse) {


if (res.state === 'success') {
console.log(res.data.title);
} else {
console.log(res.error.message);
}
}

TypeScript will complain that the res.data and res.error are


possibly undefined.

One way to solve this error is to use the optional chaining


operator:

if (res.state === 'success') {


console.log(res.data?.title);
} else {
console.log(res.error?.message);
}

But this is incorrect as we know for certain that the data object
is always defined when the state is a 'success'.

To resolve this error, you can create two types where each
represents the possible response:
type successResponse = {
state: 'success';
data: { title: string };
};

type failResponse = {
state: 'failed';
error: { message: string };
};

Then, you create a union of the two types as follows:

type ApiResponse = successResponse | failResponse;

Because the ApiResponse is now a union, TypeScript can


discriminate and narrow down the type when you use an if
statement to handle the response.

function handleResponse(res: ApiResponse) {


if (res.state === 'success') {
console.log(res.data.title);
} else {
console.log(res.error.message);
}
}

Now TypeScript knows that when the state is a 'success', the


data object is always available.

And when the state is 'failed', the error object is always


available.

Note that to use discriminating unions, you need to have one


property serve as the discriminating field.
This property needs to be a literal type like the state property
above.

If you use a common type such as a string, then TypeScript


can’t use this feature to help you narrow down the type.

Summary
TypeScript can convert the ESM syntax used in .ts files to any
valid JavaScript module format by setting the module option.

The most common format used is 'commonjs' so that your code


can run in many JavaScript runtime environments without any
configuration change.

We’ve also explored two more TypeScript features: type


assertion and discriminating unions which can be used to solve
specific problems when coding in TypeScript.

In the next chapter, we’re going to see an example of


integrating TypeScript into a JavaScript project. See you there!
CHAPTER 13: INTEGRATING
TYPESCRIPT IN JAVASCRIPT
PROJECTS

Now that you’ve learned TypeScript features and additional


syntax, it’s time to see how you can integrate TypeScript into
real world projects.

You can run TypeScript code to check .js files so that you get
type safety without having to use the .ts file.

There are two ways to do this: using JSDoc documentation


syntax or a type declaration file.

Let’s get started.

Using JSDoc to Check JavaScript Files


In the previous chapters, we have used .ts files to write
TypeScript code and compile it to JavaScript.

But this development process is only ideal for new projects that
use TypeScript from the start.
In reality, you might have a project that’s already running for a
few months or years, and then decide to integrate TypeScript
into that project.

Let’s see how you can integrate TypeScript into a plain .js file.

First, create a file named script.js and add the following code:

const BASE_API_URL = 'http://localhost:3000/tasks';


const tableElement = document.querySelector('#tbody-tasks');

export function getTasks() {


let tableRows = '';

fetch(BASE_API_URL)
.then(response => response.json())
.then(tasks => {
tasks.forEach(task => {
const element = `<h1>${task.title}</h1>`;

tableRows += element;
});
tableElement.innerHTML = tableRows;
});
}

export function deleteTask(id) {


const confirmation = confirm('Are you sure you want to delete the
task?');
if (confirmation) {
fetch(BASE_API_URL + '/' + id, {
method: 'DELETE',
}).then(response => {
if (response.ok) {
alert('Task deleted');
}
});
}
}
This code is a part of my To-do List project that’s created for the
Beginning JavaScript book.

The code has been simplified, and there are two functions here
used to get and delete tasks.

Next, you need to adjust the tsconfig.json file to allow using .js
files as follows:

{
"module": "commonjs",
"allowJs": true,
"checkJs": true,
}

The module option must be set to commonjs so that the compiled


code can run in Node.js.

The allowJs option enables you to import JavaScript files, and


the checkJs option causes TypeScript to check any .js file you
use in your project.

Now when you open the script.js file, you will start to see
errors:

1. Parameter 'task' implicitly has an 'any' type.


2. 'tableElement' is possibly 'null'.
3. Parameter 'id' implicitly has an 'any' type.

But you can’t use TypeScript syntax because this is a .js file.
For example, if you try to annotate the id parameter from the
deleteTask() function as follows:

export function deleteTask(id: number) {}

You’ll get a new error saying 'Type annotations can only be used
in TypeScript files.'

To handle this issue, you need to create documentation in JSDoc


format.

JSDoc is a special documentation format that TypeScript


supports as a way to annotate JavaScript files.

To use JSDoc, write /** just one line above the deleteTask()
definition.

You’ll see TypeScript offers the JSDoc comment auto complete


as shown below:

Press Enter, and you’ll see the documentation annotates the id


parameter with an asterisk *. Replace the asterisk with number
as follows:

/**
*
* @param {number} id
*/
And that will be enough because we only have one parameter
for the deleteTask() function.

Next, we need to annotate the task parameter, which implicitly


has an any type.

You can define a custom type using the @type tag as follows:

tasks.forEach((/** @type {{ title: string; }} */ task) => {


const element = `<h1>${task.title}</h1>`;

tableRows += element;
});

Here, the task variable is defined as an object that has a title


property of string type.

Next, we need to handle the tableElement is possibly null error.

This can be done by adding the @type tag when calling the
document.querySelector() method as follows:

const tableElement = /** @type {HTMLElement} */


(document.querySelector('#tbody-tasks'));

Now the tableElement will always be an HTMLElement.

Another way to handle this error is to add an if check before


using tableElement inside the getTasks() function:

if(tableElement) {
tableElement.innerHTML = tableRows;
}
But if you’re certain that the querySelector will never return a
null value, then using the JSDoc @type tag is recommended.

And that’s it. Now you can import and call the functions in a
TypeScript file.

In your index.ts file, you can import the functions and call
them:

import { getTasks, deleteTask } from "./script";

getTasks();

// Delete task with `id` value of one


deleteTask(1);

Run the tsc command from the terminal, and you’ll get the
generated JavaScript code in the dist/ folder.

JSDoc Disable Type Checking


If you want to disable type check for a certain .js file, you can
add the @ts-nocheck tag at the top of your file as follows:

// @ts-nocheck

const BASE_API_URL = 'http://localhost:3000/tasks';

const tableElement = document.querySelector('#tbody-tasks');

// ...

The @ts-nocheck tag causes TypeScript to stop checking that file


for type errors.
This is useful when you need more time to integrate JSDoc
properly into all your JavaScript files

As of this writing, there are some patterns and tags that


TypeScript doesn’t support yet.

For all the patterns you can use in JSDoc, you can see the
patterns supported by TypeScript at
https://g.codewithnathan.com/ts-jsdoc-support

Declaration Files
JSDoc allows you to type check JavaScript files, but notice that
you still have to modify your .js files and add annotations
there.

Another way to type JavaScript files is by adding a type


declaration file.

A type declaration file is used to describe and share type


information for your JavaScript code.

To show you an example, create a new file named script.d.ts


and write the code below:

export declare function deleteTask(id: number);

When you import the deleteTask() function in index.ts file,


TypeScript will refer to the declaration file for the type of the
function.

If you did’t pass any argument to the deleteTask() function, an


error will be shown:
import { deleteTask } from "./script";

// Delete task with `id` value of one


deleteTask();
~~~~~~~~~~

When you add a declaration file, TypeScript will refer to the


declaration file instead of the actual JavaScript file for the
definition of deleteTask.

This means you need to declare any JavaScript code you want to
use in your .ts file in the d.ts file because TypeScript doesn’t
know about your JavaScript code.

If you try to import the getTasks function like this:

import { getTasks, deleteTask } from "./script";


~~~~~~~~

You’ll get an error saying: Module '"./script"' has no exported


member 'getTasks'.

You need to create and export a declaration of getTasks()


function like this:

export declare function deleteTask(id: number);


export declare function getTasks();

Now the error disappears.

If you remove the JSDoc comments from the script.js file,


TypeScript won’t compile because you have errors in that file.

To compile the code successfully, you need to turn off the


checkJS option in tsconfig.json, or add the @ts-nocheck tag to
your JavaScript file to avoid compilation error:

"checkJs": false

The .d.ts files won’t be included in the output.

By using declarations, the JavaScript files you already created


won’t have type checkings, but you also don’t need to modify
the code in those files.

You can think of the declaration file as the specification of your


JavaScript functions, classes, and other entities that TypeScript
uses when you import them as modules in your .ts files.

Using Declaration Files From


Definitely Typed Library
Creating a declaration file is the easiest way to integrate
JavaScript code, and this is the technique used by popular
JavaScript libraries.

For example, there’s a library called lodash that provides utility


functions, and this library is written in JavaScript.

If you install the package using npm:

npm install lodash

Then try to import the library in your .ts file:

import _ from 'lodash';


~~~~~~~~
You’ll get an error saying: Could not find a declaration file for
module 'lodash'. Try npm i --save-dev @types/lodash if it exists
or add a new declaration (.d.ts) file containing d̀eclare module
'lodash'.

Many libraries are written in JavaScript, and they can’t be used


in TypeScript without adding a declaration file.

Fortunately, there’s an open collaboration by developers to


resolve this issue.

The Definitely Typed library


(https://github.com/DefinitelyTyped/DefinitelyTyped) is a
community effort to add typings for all JavaScript libraries that
are used by developers around the world.

This library has declaration files for libraries like Lodash and
React, which don’t include a declaration file yet.

To install a declaration file, use the npm install --save-dev


command followed by the @types/<package name> as follows:

npm install --save-dev @types/lodash

And just like that, the error on the .ts file is gone.

When you use any functions provided by lodash, you’ll see the
type hints as shown below:
Anytime you use a library that doesn’t have a declaration file,
try installing the @types/<package name> first to see if it already
exists.

Summary
The code for integration using JSDoc can be found at
https://g.codewithnathan.com/ts-jsdoc-integration

And the code for integration using a declaration file can be


found at https://g.codewithnathan.com/ts-declaration-file

In this chapter, you’ve learned how to integrate TypeScript into


an existing JavaScript project.

By using JSDoc, you can type your JavaScript files


incrementally, and you don’t need to convert the .js files into
.ts files.

You can also use a .d.ts declaration file so that you don’t need
to add JSDoc documentation and check the types in your
JavaScript code.

Which one of these two techniques to use? Personally, I think


using a declaration file is the way to go.

Using JSDoc requires you to modify the JavaScript code, which


can make your code more complex.

When using a declaration file, you can keep the JavaScript code
as is while providing type checks in the .d.ts file.
CHAPTER 14: TYPESCRIPT IN
NODE.JS AND EXPRESS

Now that you’ve seen how you can use TypeScript in vanilla
JavaScript code, let’s further explore integrating TypeScript in a
Node.js application.

We’re going to build a simple API for a To-do List using Node.js,
Express, and TypeScript.

I assume you already know the basics of Node.js and Express,


so this chapter focuses only on integrating TypeScript into the
project.

If you need help with learning Node.js, you can get my book at
https://g.codewithnathan.com/beginning-nodejs

Now let’s get started.

Creating a Node.js Project


Create a new folder in your computer named 'ts-express', and
open that folder in VSCode.
To initialize a Node.js project, you need to run the npm init
command.

npm init

Press Enter on all questions asked by npm until you see the
following output:

About to write to /ts-express/package.json:

{
"name": "ts-express",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}

Is this OK? (yes)

Press Enter, and you should see a package.json file generated in


the 'ts-express' folder containing the same information as
shown above.

The next step is to install the Express package:

npm install express

Next, install TypeScript and the type declarations for Node and
Express:

npm install --save-dev typescript @types/express @types/node


After that, install Nodemon for running the development
server, and ts-node so that you can run .ts files without having
to compile them to .js first:

npm install --save-dev nodemon ts-node

Now you have installed all dependencies needed for the project.

Configuring TypeScript
Run tsc --init to generate the tsconfig.json file, then slightly
change the configuration as follows:

{
"rootDir": "./src",
"outDir": "./dist",
"noImplicitAny": true,
}

Here, we organize the source code so that all TypeScript code is


placed on src/ and the compiled code is placed on the dist/
folder.

Creating a Basic Express Server


It’s time to create the Express server. First, create a folder
named src/, then create a file named index.ts with the
following content:

import express from 'express';

const app = express();

const PORT = 8000;


app.listen(PORT, () => console.log(`Server listening at port
${PORT}`));

Here, we simply create an Express application, and then set that


application to listen at port 8000.

Because of the @types/express we installed earlier, we didn’t get


any error when importing the express module.

Let’s create a simple GET route as follows:

app.get('/', (req, res) => {


res.send('Hello World!');
});

Here, we create a .get() route and simply respond with the text
'Hello World!'.

TypeScript already starts inferring the type for Express using


the declaration file, so we don’t need to add any explicit types.

Alternatively, you can type the req and res object like this:

import express, { Request, Response } from 'express';

app.get('/', (req: Request, res: Response) => {


res.send('Hello World!');
});

But it’s not needed because TypeScript already infers them


correctly.

Now that the server and route is created, open the package.json
file and change the scripts option as shown below:
"scripts": {
"start": "tsc && node dist/index.js",
"dev": "nodemon src/index.ts"
}

The start command is used for deploying to production. It


compiles the TypeScript code and produce JavaScript files in the
dist/ folder.

For development, you need to run the dev command. It uses


nodemon to run the index.ts file:

npm run dev

Now you should be able to access the API server at


localhost:8000. Nice work!

Creating an API Route


Let’s create an API route for the tasks.

Inside the src/ folder, create a new folder named routes/, then
create a new file named task.route.ts with the following
content:

import express from 'express';

const router = express.Router();

router.get('/', (req, res) => {


res.send('A list of tasks');
});

export default router;


Next, import the route in our index.ts file:

import tasksRouter from './routes/task.route';

const app = express();

// ...

app.use('/tasks', tasksRouter);

With that, you can now visit the route at localhost:8000/tasks.

We’re going to add the POST route next.

Typing Request Body With DTOs


To create a POST request, you need to use the Express JSON
middleware to parse the request body.

In your index.ts file, use the middleware as follows:

const app = express();

app.use(express.json());

Now you can create the POST route in task.route.ts like this:

router.post('/', (req, res) => {


const { title } = req.body
});

As you write the code above, hover over the title variable and
you’ll see that it’s an any type:
This is because TypeScript annotate the req.body as an any type
by default, so anything you want to unpack from the body will
also be an any type.

To define a specific type for the body object, you need to create a
custom type using either the type or interface keyword.

In the src/ folder, create a new folder named dtos/ and then
create a new file named task.dto.ts with the following content:

export interface CreateTaskDto {


title: string;
}

DTO stands for Data Transfer Object, and it’s basically just an
object that carries data in your application process.

We can use the as keyword to type the body object as the DTO
like this:

import { CreateTaskDto } from '../dtos/task.dto';

router.post('/', (req, res) => {


const { title } = req.body as CreateTaskDto;
});

Anytime you need to type the req.body object, you need to


create a DTO that specifies what you expect to get from the
request body.
This means we need to create DTOs for the PATCH and DELETE
requests later.

Modeling Data With Classes


Now that you have created a DTO for the POST request, the next
step is to create an object to save in the database.

A task should have an id, a title, and an isCompleted record


saved in the database.

Instead of always specifying the id and isCompleted value


manually, let’s create a class that serves as the model for the
task object.

In your src/ folder, create a new folder named models, then


create a new file named task.model.ts and write the following
content:

export default class Task {


id: number;
isCompleted: boolean;

constructor(public title: string){


this.id = Date.now();
this.isCompleted = false;
}
}

In the code above, we created a class named Task that has three
properties: id, title, and isCompleted.

The title property value is passed when we create an instance


of the class, while id and isCompleted have their own default
values.

To simplify the example, I use Data.now() as the value of id, but


you can replace that with your actual process.

The isCompleted property is false by default, and now we can


use the class in the POST route as follows:

import Task from '../models/task.model';

const router = express.Router();


// add an array of tasks to store data
let tasks: Task[] = [];

// get route...

router.post('/', (req, res) => {


const { title } = req.body as CreateTaskDto;
// Create a new task object
const newTask = new Task(title);
// Then save it to the tasks array
tasks.push(newTask);
res.status(201).send(newTask);
});

In the code above, we create a new Task object from the class,
then we save that new task to the tasks array.

The array here serves as a temporary database because this is


just an example. You should replace the push() code with your
actual database process.

Now you can update the GET route to send the tasks array:

router.get('/', (req, res) => {


res.status(200).send(tasks);
});
The next step is to test these API routes. Let me show you an
easy way to do it.

Testing API Routes With Bruno


If you already have an API testing program (such as Insomnia
or Postman) installed on your computer, then you can use it to
test the API routes.

If not, I recommend you download Bruno from


https://usebruno.com/downloads

Bruno is an open-source API testing client that you can use for
free.

There’s a paid version, but the free one is enough for our
purposes.

After downloading and installing Bruno, you can set the theme
to 'Dark' in Settings > Theme option.

To use Bruno, you need to create a collection, which is a folder


that will store all your API call entries.

Click on 'Create Collection', then just place the collection on the


Desktop as follows:
After creating a collection, create a new request by clicking on
the three dots menu on the collection name as shown below:
Next, fill in the request name and URL. Just put
http://localhost:8000 in the URL field as follows:
Click 'Create' and you’ll be taken to the API testing window.

Here, you can click on the arrow icon or press Enter on the URL
bar to send the request.

You’ll see the response on the right side of the window:

Now you can try sending a POST request to the /tasks URL as
follows:
Send the POST request two or three times, then send a GET
request to the same URL to see the stored tasks:

If you get the response above, it means the GET and POST
routes are working as expected. Nice work!

Adding PATCH and DELETE Routes


Now that you have the POST route completed, you only need to
create the PATCH and DELETE routes to finish the API.

First, create the DTOs in task.dto.ts as shown below:

export interface DeleteTaskDto {


id: number;
}

export interface UpdateTaskDto {


id: number;
title: string;
isCompleted: boolean;
}
Next, create the PATCH route so that you can update an existing
task:

import { CreateTaskDto, UpdateTaskDto } from '../dtos/task.dto';

// Other routes ...


router.patch('/', (req, res) => {
const { id, title, isCompleted } = req.body as UpdateTaskDto;
const task = tasks.find(task => task.id === id);
if (task) {
task.title = title;
task.isCompleted = isCompleted;
return res.status(200).send(task);
}
else {
res.status(404).send('Task not found');
}
});

Here, we use the tasks.find() method to find an existing task


with the same id value.

If found, then we update the task properties. If not, we send a


404 response code.

Next, we need to create the DELETE route as follows:

import { CreateTaskDto, UpdateTaskDto, DeleteTaskDto } from


'../dtos/task.dto';

router.delete('/', (req, res) => {


const { id } = req.body as DeleteTaskDto;

const taskToDelete = tasks.find(task => task.id === id);


if (!taskToDelete) {
return res.status(404).send('Task not found');
}
tasks = tasks.filter(task => task.id !== id);
res.status(200).send('Task deleted');
});

In the DELETE route, we use the same find() method to find the
task that we want to delete.

When the task is found, call the filter() method to remove the
task from the array.

Now the API is completed. You can test the PATCH and DELETE
route using Bruno.

Summary
The code for this project can be found at
https://g.codewithnathan.com/ts-express

When using TypeScript in a Node.js/Express project, you mostly


need to type request body objects using DTOs and model your
data using classes.

Express specific objects such as req and res are already inferred
from the @types/express library we installed, so there’s no need
to explicitly define their types.

When running the application, you can use nodemon and ts-node
to run .ts files directly.

Under the hood, ts-node compiles your application to JavaScript


before running the code, so you don’t need to run the tsc
command first.
When deploying the application, you compile the .ts files using
the same tsc command, then use Node.js to run the generated
index.js file.

And that’s how you develop a Node.js/Express application in


TypeScript.
CHAPTER 15: TYPESCRIPT IN
REACT

Hello again! In this chapter, we’re going to explore how we can


develop a React application using TypeScript.

I assume you already know how to develop web applications


with React, and you only want to know how to use TypeScript
in React.

If you need help with learning React, you can get my book at
https://codewithnathan.com/beginning-react

Now let’s get started.

Creating a React Application


First, you need to create a React application using Vite.

Vite (pronounced 'veet') is a build tool that you can use to


bootstrap a new React project.

To use Vite, open your terminal and run the following


command:
npm create vite@latest my-rts-app -- --template react-ts

The command above will use Vite to create an application


named 'my-rts-app' that uses the 'react-ts' or React TypeScript
template.

You should see npm asking to install a new package (create-vite)


as shown below. Proceed by typing 'y' and pressing Enter:

Need to install the following packages:


[email protected]
Ok to proceed? (y) y

Then Vite will create a new React project named 'my-rts-app' as


follows:

Scaffolding project in /Users/nsebhastian/dev/my-rts-app...

Done. Now run:

cd my-rts-app
npm install
npm run dev

When you’re done, Run the commands shown above in the


terminal.

Use the cd command to change the working directory to the


application we’ve just created, then run npm install to install
the packages required by the application:

cd my-rts-app
npm install
Then, we need to run the npm run dev command to start our
application:

$ npm run dev

> [email protected] dev


> vite

VITE v5.0.10 ready in 509 ms

➜ Local: http://localhost:5173/
➜ Network: use --host to expose
➜ press h + enter to show help

Now you can view the running application from the browser, at
the specified localhost address:
This means you have successfully created a React-TypeScript
application. Congratulations!

Explaining TypeScript in React


When you open the src/ folder, notice that instead of the usual
.jsx files, we have .tsx files instead.

The .tsx is an extension used to add TypeScript on top of the


usual JSX syntax.

This means you can use all React features such as components,
hooks, and Context API in .tsx files just fine.

If you open the package.json file, you’ll see there are the
@types/react and @types/react-dom among other dependencies
listed under the devDependencies property.

Because of these type libraries, TypeScript can infer the types of


React features in your code.

For example, open the App.tsx file, and see that the count state is
already typed as a :number as follows:

If you hover over the useState() hook, you can see that there’s a
generic <number> added to the function call:
You can follow these two hints and add the generic type when
calling the useState() hook.

There are only four things you need to type manually in React:

1. A model for your data


2. The props for your components
3. The generic type for the useState() hook
4. Return value of your components (optional)

I will show you how to create the custom types by building a


simple To-do List.

First, delete everything inside the App.jsx file and write a


simple component that renders a <h1> element as follows:

function App() {
return <h1>Hello World</h1>
}

export default App

Next, delete the index.css, app.css, and assets/ folder.


You also need to delete the import './index.css' statement in
your main.tsx file.

Adding Bootstrap for Styling


To make our application look nice, let’s install Bootstrap using
npm:

npm install bootstrap

Next, import the Bootstrap CSS inside the App.jsx file and test it
by adding a class to the <h1> element:

import 'bootstrap/dist/css/bootstrap.min.css';

function App() {
return <h1 className='text-primary'>Hello World</h1>
}

export default App

You should see the text color of the heading changes on the
browser:
By using Bootstrap, we don’t need to style the application from
scratch.

Modeling Data in React


Let’s start by defining the data we’re going to use in our
application.

We’re going to use a simple task data with the id and title
attributes.

First, create a new folder named models/ and then create a new
file named Task.ts with the following content:

export default interface Task {


id: number;
title: string;
}

Now that you have the data shape defined as an interface, let’s
create a component that will use this data.

Create a new folder named components/, then create a new file


named TaskList.tsx with the following content:

import Task from '../models/Task';

interface TaskListProps {
tasks: Task[];
}

function TaskList({ tasks }: TaskListProps) {


return (
<ul>
{tasks.map(task => (
<li key={task.id}>{task.title}</li>
))}
</ul>
);
}

export default TaskList;

Here, we use the Task interface when defining the


TaskListProps.

The props for this component will be a tasks array, which holds
an array of Task objects.

Inside the component, we simply render the array as a list.


Passing Generic Type to useState
Hook
Next, open the App.jsx file and import the TaskList component
as follows:

import 'bootstrap/dist/css/bootstrap.min.css';
import TaskList from './components/TaskList';
import Task from './models/Task';
import { useState } from 'react';

function App() {
const [tasks, setTasks] = useState<Task[]>([
{ id: 1, title: 'Learn TypeScript' },
]);

return (
<>
<h1>To-do List</h1>
<TaskList tasks={tasks} />
</>
);
}

export default App;

Here, we import the Task type and then pass it as a generic to


the useState() hook.

The type for the tasks state here is similar to the TaskListProps
which is an array of Task objects.

When initializing the state, we pass a single object in the shape


of the Task object:

{ id: 1, title: 'Learn TypeScript' }


If you open the application on the browser, you’ll see the task
rendered as shown below:

So far, you have created the Task interface as the model of the
data, defined the TaskListProps type for the component props,
and passed the Task interface as a generic type for the
useState() hook.

You can also type the TaskList return value as follows:

function TaskList({ tasks }: TaskListProps): JSX.Element {


// ...
}

But TypeScript already infers the type correctly as JSX.Element,


so you don’t need to explicitly type it as shown above.

Creating the Service Object


Now that we have the layout rendered, let’s continue with
fetching data for the application.

Instead of creating an API server from scratch, let’s use the


jsonplaceholder.typicode.com dummy API to fetch data from
our application.

You can visit the website at


https://jsonplaceholder.typicode.com/ to learn more, but the gist
is that you can perform an HTTP request to the dummy API to
test your application.

Inside the src/ folder, create a new folder named services, then
create a file named Task.ts with the following content:
import ky from 'ky';
import Task from '../models/Task';

class TaskService {
http = ky.create({
prefixUrl: 'https://jsonplaceholder.typicode.com/',
});

async getTasks() {
const response = await this.http.get('todos').json<Task[]>();
return response;
}
}

export default new TaskService();

The code above uses the ky library to send requests to API


endpoints.

You need to install the library using npm as follows:

npm install ky

ky simplifies the amount of code you need to write when


compared to using the plain Fetch API.

The getTasks() method above calls the /todos endpoint from the
typicode site.

You need to chain the get() call with .json() to convert the
returned data as a plain JavaScript object:

const response = await this.http.get('todos').json<Task[]>();


return response;
We also pass the Task[] as the generic type of the json() method
call to set the type of the response object.

When exporting the service, we export an instance of the class


rather than the class itself:

export default new TaskService();

This enables you to immediately use the service in the


component that imports the service, as you’ll see in the next
section.

Fetching Data With useEffect Hook


It’s time to fetch the data. In the App.jsx file, call the useEffect()
hook as shown below:

import { useEffect, useState } from 'react';


import taskService from './services/Task';

function App() {
const [tasks, setTasks] = useState<Task[]>([]);

useEffect(() => {
getData();
} ,[])

const getData = async () => {


const tasks = await taskService.getTasks();
setTasks(tasks);
}
// ...
}

In the code above, we initialize the useState() hook as an empty


array.
Next, we call the useEffect() hook, which calls the getData()
function, which calls the taskService.getTasks() method to
retrieve the data.

Once the data is fetched, we set the data to the state using the
setTasks() function.

You should now see a list of tasks on the browser as follows:

Let’s style the output a bit so it looks nicer.

On the App.jsx component, add a className prop as follows:

<div className='container'>
</div>

Next, open the TaskList.jsx file, and add the className prop to
the component as follows:

function TaskList({ tasks }: TaskListProps) {


return (
<ul className='list-group'>
{tasks.map(task => (
<li className='list-group-item' key={task.id}>{task.title}</li>
))}
</ul>
);
}

Now the output looks a bit nicer.

Deleting Task
To delete a task, you need to create a new method in the
TaskService class as follows:
async removeTask(id: number) {
const response = await this.http.delete(`todos/${id}`).json();
return response;
}

The removeTask() method above will send a DELETE request to


the todos URL.

Add a button to the TaskList component that will call the


removeTask() method on click as follows:

return (
<ul className='list-group'>
{tasks.map(task => (
<li className='list-group-item' key={task.id}>
{task.title}
<button
className='ms-3 btn btn-danger'
onClick={() => {
taskService.removeTask(task.id);
}}
>
Delete
</button>
</li>
))}
</ul>
);

Now if you click on the 'Delete' button beside the task title,
you’ll see that the task is not removed from display.

Because we use a dummy API, the resource won’t be deleted on


the typicode server.

To adjust our application, we can send a function from the


App.jsx file to remove the task. Let’s name this function
onRemoveTask() as follows:

import taskService from './services/Task';

const onRemoveTask = async (id: number) => {


await taskService.removeTask(id);
setTasks(tasks.filter(task => task.id !== id));
}

Next, pass the function to the TaskList component:

<TaskList tasks={tasks} onRemoveTask={onRemoveTask} />

Now you need to adjust the TaskListProps to include the


onRemoveTask function:

interface TaskListProps {
tasks: Task[];
onRemoveTask: (id :number) => void
}

Next, update the TaskList component to unpack the function


and call it inside the onClick function:

function TaskList({ tasks, onRemoveTask }: TaskListProps) {


return (
<button
className='ms-3 btn btn-danger'
onClick={() => onRemoveTask(task.id)}
>
Delete
</button>
);
}

With that, the tasks list will be updated each time you click on
the delete button.
Adding Task
To add a new task to the list, you can create a POST request on
the TaskService class:

async addTask(title: string) {


const response = await this.http
.post('todos', { json: { title: title } })
.json();
return response;
}

The json option in the post() method above is where you


specify the POST request body.

Next, you need to create a TaskForm component that’s going to


accept a title as input and send the POST request on submit:

import { useState } from 'react';

function TaskForm() {
const [title, setTitle] = useState('');

return (
<form>
<div className='row mb-3'>
<label htmlFor='title' className='col-2 col-form-label'>
New Task Title
</label>
<div className='col-8'>
<input
id='title'
type='text'
className='form-control'
onChange={e => setTitle(e.target.value)}
value={title}
/>
</div>
<div className='col-2'>
<button type='submit' className='btn btn-primary'>
Add Task
</button>
</div>
</div>
</form>
);
}

export default TaskForm;

Here, we simply create a form with an <input> and a <button>


element.

The input value is tied to the title state we created using the
useState() hook.

Next, we need to create a submitForm() function and pass it to


the onSubmit props of the form:

const submitForm = async (e: React.FormEvent) => {


e.preventDefault();
if (title) {
onAddTask(title);
}
};

return (
<form onSubmit={submitForm}>
// ...
</form>
)

If the title state is not empty, then the submitForm() will call the
onAddTask() function and pass title as its argument,

Next, you need to type the onAddTask that’s going to be passed to


the component as follows:
interface TaskFormProps {
onAddTask: (title: string) => void;
}

function TaskForm({ onAddTask }: TaskFormProps) {


// ...
}

The TaskForm component will be used by the App component, so


let’s open the App.tsx file and render the component:

return (
<div className='container'>
<h1>To-do List</h1>
<TaskForm onAddTask={onAddTask} />
<TaskList tasks={tasks} onRemoveTask={onRemoveTask} />
</div>
);

Above the return statement, create the onAddTask() function as


follows:

const onAddTask = async (title: string) => {


const newTask = await taskService.addTask(title) as Task;
setTasks([newTask, ...tasks ]);
}

Here, we cast the object returned by taskService.addTask()


method as a Task object, then call the setTasks() function to add
the object to the state.

Now you can add a new task to the list.


Summary
The code for this project is available at
https://g.codewithnathan.com/ts-react

Throughout this chapter, you’ve seen how TypeScript can be


integrated into a React application.

The most important thing is to create a model that represents


the data you’re going to use in your application, and then use
that model in every component that needs it.

If you’re fetching data from an API server, then you can create
a service class that’s going to contain the different request
methods for the same resource.

The component props should be typed using interfaces to


validate the data passed between components.

And that’s how you type React applications from scratch.


WRAPPING UP

Congratulations on finishing this book! We’ve gone through


many concepts and topics together to help you learn how to
code in TypeScript.

I hope you enjoyed reading and practicing TypeScript with this


book as much as I enjoyed writing it.

I’d like to ask you for a small favor.

If you enjoyed the book, I’d be very grateful if you would leave
an honest review on Amazon (I read all reviews coming my
way)

Every single review counts, and your support makes a big


difference.

Thanks again for your kind support!

Until next time,

Nathan
ABOUT THE AUTHOR

Nathan Sebhastian is a senior software developer with 8+ years


of experience in developing web and mobile applications.

He is passionate about making technology education accessible


for everyone and has taught online since 2018.
Beginning TypeScript

A Step-By-Step Gentle Guide to Master TypeScript for Beginners

By Nathan Sebhastian

https://codewithnathan.com

Copyright © 2024 By Nathan Sebhastian

ALL RIGHTS RESERVED.

No part of this book may be reproduced, or stored in a retrieval


system, or transmitted in any form or by any means, electronic,
mechanical, photocopying, recording, or otherwise, without
express written permission from the author.

You might also like