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