Introduction to TypeScript with React

Introduction to TypeScript with React

ยท

19 min read

In this article, you will learn how to use TypeScript with React.

So by the end of this article, you will have a solid understanding of how to write React code with TypeScript.

Want to watch the video version of this tutorial? You can check out the video below:

Prerequisites

To follow along with this tutorial, there are some prerequisites. You should:

  • Have basic knowledge of working with React

  • Basic understanding of writing TypeScript code

Getting Started

To get started with TypeScript, you first need to install TypeScript on your machine which you can do by executing npm install -g typescript from the terminal or command prompt.

Now, we will create a Vite project using TypeScript.

npm create vite

Once executed, you will be asked some questions.

For the project name, enter react-typescript-demo.

For framework, select React and for variant select TypeScript.

Once the project is created, open the project in VS Code and execute the following commands from the terminal:

cd react-typescript-demo
npm install

Now, let's do some code cleanup.

Remove the src/App.css file and replace the content of src/App.tsx file with the following content:

const App = () => {
  return <div>App</div>;
};

export default App;

After saving the file, you might see the red underlines in the file as shown below:

If you get that error, just press Cmd + Shift + P(Mac) or Ctrl + Shift + P(Windows/Linux)to open the VS Code command palette and enter TypeScript text in the search box and select the option TypeScript: Select TypeScript Version...

Once selected, you will see options to select between the VS Code version and the workspace version as shown below:

From these options, you need to select Use Workspace Version option and once you select that option, the error from the App.tsx file will be gone.

Now, open src/index.css file and replace its contents with the following content:

:root {
  font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
  line-height: 1.5;
  font-weight: 400;

  font-synthesis: none;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  -webkit-text-size-adjust: 100%;
}

Now, let's start the application, by executing npm run dev command.

Now, click on the displayed URL and access the application, you will see the following initial screen with text App displayed in the browser.

React and TypeScript Basics

When using React with TypeScript, the first thing you should know is the file extension.

Every React + TypesSript file needs 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.

So open src/App.tsx file and replace it with the following contents:

import { FC } from 'react';

const App: FC = () => {
  return <div>App</div>;
};

export default App;

Now, let's pass some props to this App component.

Open src/main.tsx and pass a title prop to the App component as shown below:

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.tsx';
import './index.css';

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <App title='TypeScript Demo' />
  </React.StrictMode>
);

However, with the added title prop, we now have a TypeScript error as you can see below:

Three Ways of Defining Prop Types

We can fix the above TypeScript error in three different ways.

  • Declaring Types Using Interface

The error is coming because we have added a title prop as a mandatory prop for the App component so we need to mention this inside the App component.

So open src/App.tsx file and replace its contents with the following content:

import { FC } from 'react';

interface AppProps {
  title: string;
}

const App: FC<AppProps> = () => {
  return <div>App</div>;
};

export default App;

As you can see above, we have added an extra interface AppProps to specify which props the component is accepting and we used the AppProps interface after the FC in angle brackets.

It's a good practice and recommended to start the interface name with a capital letter like AppProps in our case.

Now, with this change, the TypeScript error will be gone as you can see below:

This is how we specify what props a particular component accepts.

  • Declaring Types Using type

We can also declare the props type using type keyword.

So open App.tsx file and change the below code:

import { FC } from 'react';

interface AppProps {
  title: string;
}

const App: FC<AppProps> = () => {
  return <div>App</div>;
};

export default App;

to this code:

import { FC } from 'react';

type AppProps = {
  title: string;
};

const App: FC<AppProps> = () => {
  return <div>App</div>;
};

export default App;

Here, instead of interface declaration we used type declaration and the code will work without any TypeScript error.

It's up to you, which way to use, I always like to use an interface for declaring component types.

  • Using inline type declaration

The third way of declaring type is by defining inline types as shown below:

const App = ({ title }: { title: string }) => {
  return <div>App</div>;
};

export default App;

As you can see above, we have removed the use of FC as it's not needed, and while destructuring the title prop we defined the type of it.

So out of these three ways, you can use whichever way you want. I always prefer to use an interface with FC so if later I want to add more props, the code will not look complicated which will happen if you define inline types.

Now, let's use the title prop and display it on the UI.

Now, replace the contents of the App.tsx file with the following content:

import { FC } from 'react';

interface AppProps {
  title: string;
}

const App: FC<AppProps> = ({ title }) => {
  return <h1>{title}</h1>;
};

export default App;

As you can see, we're using an interface with FC and we're destructuring the title prop and displaying it on the screen.

Now, open src/index.css file and add the following CSS inside it:

h1 {
  text-align: center;
}

If you check the application in the browser, you will see that the title with text TypeScript Demo is correctly getting displayed.

How to Create a Random Users List Application

Now, that you have a basic idea of how to declare component props, let's create a simple Random Users List Application that will display a list of 10 random users on the screen.

For that, we will be using Random User Generator API.

And the API URL we will be using is this:

https://randomuser.me/api/?results=10

So let's first install the axios npm library, so we can make an API call using it.

Execute the following command to install the axios library:

npm install axios

Once installed, restart the application by executing npm run dev command.

Now, replace the contents of App.tsx file with the following content:

import axios from 'axios';
import { FC, useEffect } from 'react';

interface AppProps {
  title: string;
}

const App: FC<AppProps> = ({ title }) => {
  useEffect(() => {
    const getUsers = async () => {
      try {
        const { data } = await axios.get(
          'https://randomuser.me/api/?results=10'
        );
        console.log(data);
      } catch (error) {
        console.log(error);
      }
    };
    getUsers();
  }, []);

  return <h1>{title}</h1>;
};

export default App;

As you can see above, we have added an useEffect hook where we're making API call to get the the list of users.

Now, If you open the console in the browser, you will be able to see the API response displayed in the console.

As you can see, we're correctly getting a list of 10 random users and the actual users list is coming in the results property of the response.

How to Store the Users List in State

Now, let's store those users in the state so we can display them on the screen.

So inside the App component, declare a new state with an initial value of an empty array like this:

const [users, setUsers ] = useState([]);

And call the setUsers function to store the users in the useEffect hook after the API call.

So your App component will look like this now:

import axios from 'axios';
import { FC, useEffect, useState } from 'react';

interface AppProps {
  title: string;
}

const App: FC<AppProps> = ({ title }) => {
  const [users, setUsers] = useState([]);

  useEffect(() => {
    const getUsers = async () => {
      try {
        const { data } = await axios.get(
          'https://randomuser.me/api/?results=10'
        );
        console.log(data);
        setUsers(data.results);
      } catch (error) {
        console.log(error);
      }
    };
    getUsers();
  }, []);

  return <h1>{title}</h1>;
};

export default App;

As you can see here, we're calling setUsers function with the value of data.results.

How to Display the Users on the UI

Now, let's display the name and email of the individual user on the screen.

If you check the console output, you can see that there is a name property for each object that contains the first and last name of the user. So we can combine them to display the complete name.

Also, we have a direct email property for each user object which we can use to display email.

So replace the contents of App.tsx file with the following content:

import axios from 'axios';
import { FC, useEffect, useState } from 'react';

interface AppProps {
  title: string;
}

const App: FC<AppProps> = ({ title }) => {
  const [users, setUsers] = useState([]);

  useEffect(() => {
    const getUsers = async () => {
      try {
        const { data } = await axios.get(
          'https://randomuser.me/api/?results=10'
        );
        console.log(data);
        setUsers(data.results);
      } catch (error) {
        console.log(error);
      }
    };
    getUsers();
  }, []);

  return (
    <div>
      <h1>{title}</h1>
      <ul>
        {users.map(({ login, name, email }) => {
          return (
            <li key={login.uuid}>
              <div>
                Name: {name.first} {name.last}
              </div>
              <div>Email: {email}</div>
              <hr />
            </li>
          );
        })}
      </ul>
    </div>
  );
};

export default App;

As you can see, we're using the array map method to loop over the users array, and we're using object destructuring to destructure the login, name and email properties of individual user object, and as an un-ordered list we're displaying the name and email of the user.

However, you will see some TypeScript errors in the file as can be seen below:

This is because, as you can see above, by default TypeScript assumes the type of users array to be never[], so it's not able to find out which properties the users array contains.

So we specifically need to specify all of the properties that we're using along with their types.

Therefore, declare a new interface after the AppProps interface like this:


interface Users {
  name: {
    first: string;
    last: string;
  };
  login: {
    uuid: string;
  };
  email: string;
}

Here, we're specifying that, each individual user will be an object with name, login and email properties, and we're also specifying the data type of each property.

As you can see, each user object coming from the API has a lot of other properties like phone, location and others. But we only need to specify those properties that we're using in the code.

Now, change the useState users array declaration from this:

const [users, setUsers] = useState([]);

to this:

const [users, setUsers] = useState<Users[]>([]);

Here, we're specifying that users is an array of objects of type Users which is the interface we declared.

Now, If you check the App.tsx file, you will see that there are no TypeScript errors.

And you will be able to see the list of 10 random users displayed on the screen:

As you have seen previously, we have declared Users interface like this:

interface Users {
  name: {
    first: string;
    last: string;
  };
  login: {
    uuid: string;
  };
  email: string;
}

However, when you have nested properties, you will see it written like this:

interface Name {
  first: string;
  last: string;
}

interface Login {
  uuid: string;
}

interface Users {
  name: Name;
  login: Login;
  email: string;
}

The advantage of declaring separate interfaces for each nested property is that, If you want to use the same structure in any other file, you can just export any of the above interfaces and re-use in other files, Instead of re-declaring the same interface again.

So let's export all of the above interfaces as a named export. So the code will look like this:

export interface Name {
  first: string;
  last: string;
}

export interface Login {
  uuid: string;
}

export interface Users {
  name: Name;
  login: Login;
  email: string;
}

As I said previously, you can also use type declaration here instead of using the interface, so it will look like this:

type Name = {
  first: string;
  last: string;
};

type Login = {
  uuid: string;
};

type Users = {
  name: Name;
  login: Login;
  email: string;
};

How to Create a Separate User Component

When we're using an array map method, to display something on the screen, it's common to separate out that display part in a different component so it's easy to test and it will also make your component code shorter.

So create a components folder inside src folder and create User.tsx file inside it and add the following contents inside it:

const User = ({ login, name, email }) => {
  return (
    <li key={login.uuid}>
      <div>
        Name: {name.first} {name.last}
      </div>
      <div>Email: {email}</div>
      <hr />
    </li>
  );
};

export default User;

If you save the file, you will see the TypeScript errors again.

So we again need to specify which props the User component will be receiving, and we also need to specify the data type of each of them.

So the updated User.tsx file will look like this:

import { FC } from 'react';
import { Login, Name } from '../App';

interface UserProps {
  login: Login;
  name: Name;
  email: string;
}

const User: FC<UserProps> = ({ login, name, email }) => {
  return (
    <li key={login.uuid}>
      <div>
        Name: {name.first} {name.last}
      </div>
      <div>Email: {email}</div>
      <hr />
    </li>
  );
};

export default User;

As you can see above, we have declared a UserProps interface above and we have specified it for the User component using FC.

Also, note that we're not declaring the data type of name and login properties, instead we're using the exported types from App.tsx file:

import { Login, Name } from '../App';

That's why it's good to declare separate types for each nested property, so we can reuse it elsewhere.

Now, we can use this User component inside the App.tsx file.

So change the below code:

{users.map(({ login, name, email }) => {
  return (
    <li key={login.uuid}>
      <div>
        Name: {name.first} {name.last}
      </div>
      <div>Email: {email}</div>
      <hr />
    </li>
  );
})}

to this code:

{users.map(({ login, name, email }) => {
  return <User key={login.uuid} name={name} email={email} />;
})}

As you might know when using the array map method, we need to provide the key for the parent element which is User in our case so we have added key prop while using the User component as shown above.

So that means we don't need key inside the User component, so we can remove the key and login prop from the User component.

So the updated User component will look like this:

import { FC } from 'react';
import { Name } from '../App';

interface UserProps {
  name: Name;
  email: string;
}

const User: FC<UserProps> = ({ name, email }) => {
  return (
    <li>
      <div>
        Name: {name.first} {name.last}
      </div>
      <div>Email: {email}</div>
      <hr />
    </li>
  );
};

export default User;

As you can see we have removed the login prop from the interface and also while destructuring it and the application is still working as before without any issues as you can see below.

How to Create a Separate File for Type Declarations

As you can see, the App.tsx file has become quite large because of the interface declarations, so it's common to have a separate file just for declaring types.

So create a App.types.ts file inside the src folder and move all the type declarations from App component to App.types.ts file:

export interface AppProps {
  title: string;
}

export interface Name {
  first: string;
  last: string;
}

export interface Login {
  uuid: string;
}

export interface Users {
  name: Name;
  login: Login;
  email: string;
}

Note that, we're also exporting the AppProps component in the above code.

Now, update the App.tsx file to use these types as shown below:

import axios from 'axios';
import { FC, useEffect, useState } from 'react';
import { AppProps, Users } from './App.types';
import User from './components/User';

const App: FC<AppProps> = ({ title }) => {
  const [users, setUsers] = useState<Users[]>([]);

  // ...
};

export default App;

As you can see above, we're importing the AppProps and Users from App.types file:

import { AppProps, Users } from './App.types';

And your User.tsx file will look like this now:

import { FC } from 'react';
import { Name } from '../App.types';

interface UserProps {
  name: Name;
  email: string;
}

const User: FC<UserProps> = ({ name, email }) => {
  return (
    <li>
      <div>
        Name: {name.first} {name.last}
      </div>
      <div>Email: {email}</div>
      <hr />
    </li>
  );
};

export default User;

As you can see above, we're importing the Name from App.types file.

import { Name } from '../App.types';

How to Display a Loading Indicator

Whenever you're making an API call to display something, it's always good to display some loading indicator, while the API call is going on.

So let's add a new isLoading state inside the App component:

const [isLoading, setIsLoading] = useState(false);

As you can see, we have not mentioned any data type while declaring a state like this:

const [isLoading, setIsLoading] = useState<boolean>(false);

This is because when we assign any initial value, false in our case, TypeScript automatically infers the type of data we will be storing which is boolean in our case.

When we declared users state, it was not clear what we would be storing by just the initial value of an empty array [] so we needed to mention its type like this:

const [users, setUsers] = useState<Users[]>([]);

Now, change the useEffect code to the below code:

useEffect(() => {
  const getUsers = async () => {
    try {
      setIsLoading(true);
      const { data } = await axios.get(
        'https://randomuser.me/api/?results=10'
      );
      console.log(data);
      setUsers(data.results);
    } catch (error) {
      console.log(error);
    } finally {
      setIsLoading(false);
    }
  };
  getUsers();
}, []);

Here, we're calling setIsLoading with value of true before the API call, and inside the finally block, we're setting it back to false.

The code written inside the finally block will always execute even if there is success or failure. So even if the API call succeeds or fails, we need to hide the loading message so we're using finally block to achieve that.

Now, we can use the isLoading state value to display a loading message on the screen.

After the h1 tag and before the ul tag, add the following code:

{isLoading && <p>Loading...</p>}

Now, If you check the application, you will be able to see the loading message while the list of users is getting loaded.

So this is a better user experience.

However, If you keep a closer look at the displayed users, you will see that the users are getting changed once loaded.

So initially we see a set of 10 random users and immediately we see a different set of random users without reloading the page.

This is because we're using React version 18(which you can verify from package.json file) and React.StrictMode inside the src/main.tsx file.

And with version 18 of React, when we use React.StrictMode, every useEffect hook executes twice even with no dependency specified.

This only happens in the development environment and not in the production when you deploy the application.

Because of this, the API call is made twice and as the random users API returns a new set of random users every time the API is called, we're setting a different set of users to the users array using setUsers call inside the useEffect hook.

This is the reason we see users getting changed without refreshing the page.

If you don't want this behaviour during development, you can remove the React.StrictMode from the main.tsx file.

So change the below code:

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.tsx';
import './index.css';

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <App title='TypeScript Demo' />
  </React.StrictMode>
);

to this code:

import ReactDOM from 'react-dom/client';
import App from './App.tsx';
import './index.css';

ReactDOM.createRoot(document.getElementById('root')!).render(
  <App title='TypeScript Demo' />
);

Now, If you check the application, you will see that the list of users is not getting changed, once loaded.

How to Load Users on Button Click

Now, instead of making the API call on page load, let's add a show users button and we will make the API call when we click on that button.

So after h1 tag add a new button as shown below:

<button onClick={handleClick}>Show Users</button>

Now, add handleClick method inside the App component and move all the code from getUsers function to the handleClick method:

const handleClick = async () => {
  try {
    setIsLoading(true);
    const { data } = await axios.get('https://randomuser.me/api/?results=10');
    console.log(data);
    setUsers(data.results);
  } catch (error) {
    console.log(error);
  } finally {
    setIsLoading(false);
  }
};

Now, you can remove or comment out the useEffect hook as it's no longer needed.

Your updated App.tsx file will look like this now:

import axios from 'axios';
import { FC, useState } from 'react';
import { AppProps, Users } from './App.types';
import User from './components/User';

const App: FC<AppProps> = ({ title }) => {
  const [users, setUsers] = useState<Users[]>([]);
  const [isLoading, setIsLoading] = useState<boolean>(false);

  // useEffect(() => {
  //   const getUsers = async () => {
  //     try {
  //       setIsLoading(true);
  //       const { data } = await axios.get(
  //         'https://randomuser.me/api/?results=10'
  //       );
  //       console.log(data);
  //       setUsers(data.results);
  //     } catch (error) {
  //       console.log(error);
  //     } finally {
  //       setIsLoading(false);
  //     }
  //   };
  //   getUsers();
  // }, []);

  const handleClick = async () => {
    try {
      setIsLoading(true);
      const { data } = await axios.get('https://randomuser.me/api/?results=10');
      console.log(data);
      setUsers(data.results);
    } catch (error) {
      console.log(error);
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <div>
      <h1>{title}</h1>
      <button onClick={handleClick}>Show Users </button>
      {isLoading && <p>Loading...</p>}
      <ul>
        {users.map(({ login, name, email }) => {
          return <User key={login.uuid} name={name} email={email} />;
        })}
      </ul>
    </div>
  );
};

export default App;

Now, If you check the application, you will see that users are loaded only when clicking on the show users button and we also see the loading message.

How to Handle the Change Event

Now, let's add an input field and when we type anything in that input field, we will display the entered text below that input field.

Add an input field after the button like this:

<input type='text' onChange={handleChange} />

And declare a new state to store the entered value like this:

const [username, setUsername] = useState('');

Now, add a handleChange method inside the App component like this:

 const handleChange = (event) => {
  setUsername(event.target.value);
};

However, you will see that, we're getting a TypeScript error for the event parameter.

With TypeScript we always need to specify the type of each and every function parameter.

Here, TypeScript is not able to identify what's the type of event parameter.

To find out the type of event parameter, we can change the below code:

<input type='text' onChange={handleChange} />

to this code:

<input type='text' onChange={(event) => {}} />

Here, we're using an inline function because when using inline function, the correct type is automatically passed to the function parameter so we don't need to specify it.

So If you mouse over the event parameter, you will be able to see the exact type of event that we can use in our handleChange function as you can see below:

Now, you can revert the below code:

<input type='text' onChange={(event) => {}} />

to this code:

<input type='text' onChange={handleChange} />

Now, let's display the value of username state variable below the input field:

<input type='text' onChange={handleChange} />
<div>{username}</div>

If you check the application now, you will be able to see the entered text displayed below the input field.

Thanks for Reading

That's it for this tutorial. I hope you learned a lot from it.

Want to watch the video version of this tutorial? You can check out this video.

You can find the complete source code for this application in this repository.

If you want to master JavaScript, ES6+, React, and Node.js with easy-to-understand content, check out my YouTube channel. Don't forget to subscribe.

Want to stay up to date with regular content on JavaScript, React, and Node.js? Follow me on LinkedIn.

Did you find this article valuable?

Support Yogesh Chavan by becoming a sponsor. Any amount is appreciated!