TypeScript is a powerful programming language that enhances the development of complex web applications by adding a static type system to JavaScript. React is a popular JavaScript library used for building user interfaces. Combining these two technologies can take your web development to the next level.
In this article, you will learn the basics of TypeScript with React in the first sections of the article. And then you will learn to build a contact manager app using React and TypeScript.
So by the end of this article, you will have a clear understanding of how to properly use TypeScript with React. If you want to refer the code for this entire project, you can download it from here.
So let’s get started.
React And TypeScript Basics
When learning React with TypeScript, the first thing you should understand is the file extension.
Every React + TypesSript file need to have a .tsx
extension.
If the file does not contain any JSX-specific code, then you can use the .ts
extension instead of the .tsx
extension.
To create a component in React with TypeScript, you can use the FC
type from the react
package and use it after the component name.
Create a new file with the name Header.tsx
and add the following code:
import { FC } from 'react';
const Header: FC = () => {
return Welcome, Mike!/h1>;
};
export default Header;
Code language: TypeScript (typescript)
Here, we’re just displaying a welcome message in a functional component.
Now, If we want to pass a dynamic name
as a prop we can do that like this:
import { FC } from 'react';
interface HeaderProps {
name: string;
}
const Header: FC= ({ name }) => {
return Welcome, {name}!/h4>;
};
export default Header;
Code language: TypeScript (typescript)
Here, we have declared HeaderProps
as an interface that has a name
property and for the Header
component, we have added angle brackets to use that interface.
This way TypeScript will know that the Header
component needs a name
prop which is a required prop.
And we can call the component like this:
Code language: TypeScript (typescript)'Jerry' />
If you miss any of the required props like name
in our case, then TypeScript will throw an error as can be seen below:
So whatever props the component interface is designed to take has to be provided unless they’re optional.
If some of the properties are optional and not required all the time then we can use the ?
operator to indicate that it’s an optional prop like this:
interface HeaderProps {
name: string;
isMarried?: boolean;
}
Code language: TypeScript (typescript)
Here, isMarried
is made an optional prop because of the ?
at the end and we can use it like this:
const Header: FC= ({ name, isMarried }) => {
return (
Welcome, {name}!/h4>
Marital status: {isMarried ? 'Married' : 'Not Provided'}p>
/>
);
};
Code language: TypeScript (typescript)
So, If the isMarried
prop is provided, you will see the Married
status otherwise Not Provided
status as shown below:
In the above code, we have used an interface to declare the type of props passed.
But you can also create a type as shown below:
type HeaderProps = {
name: string,
isMarried?: boolean
};
Code language: TypeScript (typescript)
And use it the same way as used for the interface.
It’s up to you which one to choose, however in most of the code, you will see the interface used instead of the type.
Alternative Way of Declaring Component Props Types
In the above code, we have used : FC
to specify the props types.
There is another way you will find in some of the React + TypeScript codebases.
So instead of declaring a Header
component like this:
const Header: FC= ({ name, isMarried }) => {
// ...
};
Code language: TypeScript (typescript)
we can declare it like this:
const Header = ({ name, isMarried }: HeaderProps) => {
// ...
};
Code language: TypeScript (typescript)
As you can see, we have specified the TypeScript type while destructuring the props.
How to Work With Event Handlers
It’s common to have some event handlers in the component for events like click event, change event, etc.
So let’s see how to work with them when using TypeScript.
Take a look at the below code:
import { FC, useState } from 'react';
const UserSearch: FC = () => {
const [searchTerm, setSearchTerm] = useState('');
const handleSearch = (event) => {};
return (
type='text'
name='searchTerm'
value={searchTerm}
placeholder='Type something!'
onChange={handleSearch}
/>
{searchTerm && SearchTerm: {searchTerm}/div>}
>
/>
);
};
export default UserSearch;Code language: TypeScript (typescript)
Here, we’re displaying an input field where users enter something and we’re displaying that value below that input.
But as you can see, we’ve not specified any type for the `event` parameter of the handleSearch
function which we explicitly need to specify otherwise we will get a TypeScript error as shown below:
As you can see in the above screenshot, when the mouse hovered over the event
parameter, TypeScript gives an error saying, “Parameter event
implicitly has an any
type.”
To fix that, we first need to identify the exact type of event.
To do that, temporarily change the below code:
onChange = { handleSearch };
Code language: TypeScript (typescript)
to this code:
onChange = { (event) => {} };
Code language: TypeScript (typescript)
Here, we’re using an inline function so, If you mouse over the event parameter, you will see the event type `React.ChangeEvent` which we can use for the event parameter as shown below:
So by just adding an inline function, you can easily identify the event type of any event handler added.
Now, as we got the event type, we can set the searchTerm
value using the setSearchTerm
function as shown below:
const handleSearch = (event: React.ChangeEventHTMLInputElement>) => {
setSearchTerm(event.target.value);
};
Code language: TypeScript (typescript)
And the search is working as shown below:
Now, you know the basics of React + TypeScript, let’s build a contact manager app so will you get hands-on experience with React + TypeScript.
How to Setup a TypeScript + React Project
**You can see the final demo of the application we’re building here.
To set up the app we will be using Vite. It’s a popular and faster alternative to create-react-app.
We’ll use Vite because create-react-app
becomes slow when the application grows and takes a lot of time to refresh the page when we make any changes in the application code. Also, by default, it adds a lot of extra packages which we rarely need.
Vite just rebuilds the things we changed, instead of rebuilding the entire application which saves a lot of time during development.
Keep in mind that Vite requires Node.js version 14.18+, so make sure to install a Node version greater than or equal to 14.18.
The easiest and simplest way to install and switch Node.js versions is to use [nvm].
Even if you’re using create-react-app, all the code you will learn in this tutorial should run exactly the same without any errors.
To create a new Vite project with React and TypeScript, execute the npm init vite
command from the terminal.
It will ask you for the project name
, framework
, and variant
.
– For project name
, you can enter contact-manager-app-typescript
or any name of your choice.
– For framework
, select React
from the list of options
– For variant
, select TypeScript
from the list of options
Once the project is created, you can open that project in your favorite IDE like Visual Studio Code.
The project folder structure will look like this:
Now, execute the yarn install
or npm install
command from inside the project folder to install all the packages from the package.json
file.
Once all the packages are installed, you can execute the yarn run dev
or npm run dev
command to start the created React application.
As you can see the application can be accessed on the URL http://localhost:5173/
.
Initial Project Setup
Install the bootstrap
, react-bootstrap
and react-icons
npm packages by executing the following command from the project folder:
yarn add bootstrap@5.2.3 react-bootstrap@2.7.0 react-icons@4.7.1
Code language: TypeScript (typescript)
or with npm:
npm install bootstrap@5.2.3 react-bootstrap@2.7.0 react-icons@4.7.1
Code language: TypeScript (typescript)
Here, we’re installing the latest and specific versions of packages so you will not have any issues running the application If in the future there is a newer version of any of the packages.
Now, open the index.css
file and add the following contents inside it:
* {
padding: 0;
margin: 0;
box-sizing: border-box;
}
body {
font-family: Inter, sans-serif;
padding: 1rem;
letter-spacing: 1px;
background-color: #f7f6f9;
color: #7b774e;
}
.main-container {
margin: 1rem auto;
display: flex;
justify-content: start;
align-items: baseline;
}
.btn,
.btn:active,
.btn:hover {
border: none;
background-color: #3b8855d6;
}
.icon {
cursor: pointer;
}
.errorMsg {
color: #f21e08;
background: #fff0f0;
padding: 10px;
}
h1 {
text-align: center;
}
.contact-form {
margin: 1rem auto;
width: 45%;
}
.modal-dialog .contact-form {
width: 100%;
}
.submit-btn {
margin-top: 1rem;
letter-spacing: 1px;
}
.contacts-list-table-container {
max-width: 100%;
height: 500px;
overflow: auto;
}
.contacts-list {
display: flex;
width: 45%;
flex-direction: column;
justify-content: flex-start;
align-items: center;
}
.contacts-list-title {
margin-bottom: 1.2rem;
}
.contacts-list-table,
.contacts-list-table tr,
.contacts-list-table th,
.contacts-list-table td {
padding: 5px 20px;
}
.contacts-list-table {
border-collapse: collapse;
width: 100%;
}
.contacts-list-table th,
.contacts-list-table td {
border: 1px solid #dddddd;
text-align: left;
padding: 8px;
}
.contacts-list-table tr:nth-child(even) {
background: #ecebee;
}
.contacts-list-header {
position: sticky;
top: 0;
background: #ecebee;
}
@media screen and (max-width: 900px) {
.contact-form,
.contacts-list,
.main-container {
flex-direction: column;
align-items: center;
width: 100%;
}
}
Code language: CSS (css)
Now, open the main.tsx
file and add the bootstrap CSS import before the index.css import as shown below:
import 'bootstrap/dist/css/bootstrap.min.css';
import './index.css'
Code language: TypeScript (typescript)
How to Create the Initial Pages
Now, create a components
folder inside the src
folder and create a Header.tsx
file inside it.
import { FC } from 'react';
const Header: FC = () => {
return (
Contact Manager App/h1>
header>
);
};
export default Header;
Code language: TypeScript (typescript)
Here, we have declared the Header
component using FC
which is a TypeScript way of declaring a component as a functional component.
If you want, you can skip the : FC
as the code will work without mentioning it also but it’s always good to specify it explicitly.
Now, Open the App.tsx
file and replace it with the following contents:
import Header from './components/Header';
function App() {
return (
'App'>
/div>
);
}
export default App;Code language: TypeScript (typescript)
Now, If you start the application using yarn run dev
or npm run dev
and access it at URL http://localhost:5173/
, you will see the following screen:
If you want, you can open the index.html
file and change the title of the page to Contact Manager App
instead of the Vite + React + TS
.
Now, create a new file ContactForm.tsx
inside the components
folder and add the following code inside it:
import { FC, useState } from 'react';
import { Button, Form } from 'react-bootstrap';
const ContactForm: FC = () => {
const [contact, setContact] = useState({
firstName: '',
lastName: '',
phone: ''
});
const handleOnChange = (event) => {
const { name, value } = event.target;
setContact((prevState) => {
return {
...prevState,
[name]: value
};
});
};
const handleOnSubmit = (event) => {
event.preventDefault();
};
return (
'firstName'>
First Name/Form.Label>
>
/Form.Group>
Last NameForm.Label>
'lastName'
name='lastName'
value={contact.lastName}
type='text'
onChange={handleOnChange}
/>
/Form.Group>
PhoneForm.Label>
'phone'
name='phone'
value={contact.phone}
type='number'
onChange={handleOnChange}
/>
/Form.Group>
Code language: TypeScript (typescript)
In the above code, we’re displaying a form with three input fields namely, first name, last name, phone, and a submit button.
For displaying the inputs, we’re using the Form.Control
component from react-bootstrap
so the UI will look nice and we don’t need to write a lot of CSS ourselves.
All the above code is a pure React code without any TypeScript Code, so you will see red underlines for the event
parameter of the handleOnChange
and handleOnSubmit
methods as can be seen in the below screenshot:
As you can see in the above screenshot, when you mouse hover over the event
parameter, you can see the TypeScript error saying, “Parameter
event
implicitly has an any
type.”
To fix that, we first need to identify the exact type of event we need to provide.
To do that, temporarily change the below code:
onChange = { handleOnChange };
Code language: TypeScript (typescript)
to this code:
onChange = { (event) => {} };
Code language: TypeScript (typescript)
Here, we’re using an inline function so, if you mouse over the event parameter, you will see the event type React.ChangeEvent
which we can use for the event parameter as shown below:
However, as we know, the element for which the onChange
handler is added is an input element so instead of React.ChangeEvent
, we can use React.ChangeEvent
so we don’t need to import any extra TypeScript specific type.
With this change, you can see the TypeScript error is gone.
Similarly, we can find out the event parameter type for the onSubmit
handler as shown below:
Event Type Submit
So using this simple way, we can find out the event type of any change or submit handler function.
Now, you can revert the onChange and onSubmit handlers from inline functions to the respective handler function:
onChange = { handleOnChange };
onSubmit = { handleOnSubmit };
Code language: TypeScript (typescript)
Now, open the App.tsx
file, and let’s add the ContactForm
component below the Header
component.
So your App.tsx
file will look like this:
import ContactForm from './components/ContactForm';
import Header from './components/Header';
function App() {
return (
'App'>
'main-container'>
/div>
div>
);
}
export default App;Code language: TypeScript (typescript)
And If you check the application, you will see the following screen:
Let’s add a console.log
statement in the handleOnSubmit
method as shown below so we can see the data submitted.
const handleOnSubmit = (event: React.FormEventHTMLFormElement>) => {
event.preventDefault();
console.log(contact);
};
Code language: TypeScript (typescript)
As you can see, we’re correctly able to store the details in the state with the name contact
and display it on the console.
How to Use useReducer Hook For Storing Contacts
Now, we need to display the added contacts on the page.
To do that, we first need to store all the contacts together.
So, we will use the useReducer
hook for that using which we can easily handle the edit and delete contact functionality.
If you’re not aware of the useReducer
hook, then check out this article.
Create a new folder with the name reducer
inside the src
folder and create a contactsReducer.ts
file inside it with the following contents:
export const contactsReducer = (state, action) => {
switch (action.type) {
case 'ADD_CONTACT':
return {
...state,
contacts: [...state.contacts, action.payload]
};
default:
return state;
}
};
Code language: TypeScript (typescript)
Note that the file extension is .ts
and not .tsx
because there is no JSX code inside it.
When you save the file, you will see a red underline for the state
and action
parameters as shown below:
So let’s define the TypeScript types for the state
and action
parameters as shown below:
export interface Contact {
firstName: string;
lastName: string;
phone: string;
}
export interface Action {
type: 'ADD_CONTACT'
payload: Contact;
}
export interface State {
contacts: Contact[];
}
export const contactsReducer = (state: State, action: Action): State => {
switch (action.type) {
case 'ADD_CONTACT':
return {
...state,
contacts: [...state.contacts, action.payload]
};
default:
return state;
}
};
Code language: TypeScript (typescript)
In the above code, we’re also explicitly defining State
as the return type of the contactsReducer
function as can be seen after the arrow syntax (=>):
export const contactsReducer = (state: State, action: Action): State => {
Code language: TypeScript (typescript)
If you have worked with redux before, you might know that action
always contains a type
property and an optional payload
.
So we have defined an Action
interface to indicate the action
type.
As the contact form contains the firstName
, lastName
, and phone
properties, we’re declaring an interface Contact
indicating that it will be the type of payload.
We’re also exporting those interfaces so we can use them in other files if required.
Also, we have defined state
as an object with a contacts
property.
The contacts
property will be an array containing only the firstName
, lastName
, and phone
properties so we have defined it as shown below:
export interface State {
contacts: Contact[];
}
Code language: TypeScript (typescript)
So the contacts
property will always contain an array of objects of the Contact
type.
Now, open the App.tsx
file and use the contactsReducer
as the first argument for the useReducer
hook as shown below:
import { useReducer } from 'react';
import ContactForm from './components/ContactForm';
import Header from './components/Header';
import { contactsReducer, State } from './reducer/contactsReducer';
const initialState: State = {
contacts: []
};
function App() {
const [state, dispatch] = useReducer(contactsReducer, initialState);
return (
'App'>
'main-container'>
/div>
div>
);
}
export default App;Code language: TypeScri