React TypeScript

How to Build React Apps with TypeScript with a Practical Example

Spread the love


TypeScript is a superset of JavaScript that was created to address many common problems — the biggest being that JavaScript variables and objects have all dynamic types. This means that you have no way to know what properties an object has without logging it in a debugger.

This creates a lot of frustration as you have to check each individual object yourself, slowing down development. Without static types, you also cannot have auto-complete in your editor since there is no way to know what are in those objects are with 100% certainty.

Also, you can put any argument into your JavaScript functions, so there is no enforcement to what is passed in. This creates problems when you pass the wrong argument types expected or don’t pass in enough arguments, making those parameters undefined. There is also nothing stopping you from passing in too many arguments.

By being able to detect all these issues at compile-time, TypeScript makes code easier to understand and follow while decreasing the number of bugs sent to production. You don’t have to worry about breaking things when you change code as the compiler will tell you that you got those basic errors.

Create React App projects can be started with TypeScript using the --typscript option. However, we need to install type definitions for some libraries ourselves.

In this article, we will build an address book app using React and TypeScript.

Getting Started

To start, we need to run Create React App to scaffold the app. We run npx create-react-app address-book --typscript to create the app project folder with the initial files. The app will have a home page to display the contacts and let us open a modal to add a contact. There will be a table that displays all the contacts, and Edit and Delete buttons on each row.

The contacts will be stored in a central MobX store, making them easy to access. React Router will be used for routing. Contacts will be saved in the back end by using the JSON server. We will also use Formik and Yup for form management and validation.

To install the libraries we mentioned above, we run npm i axios bootstrap formik react-bootstrap mobx mobx-react react-router-dom yup. Axios is the HTTP client that we use for making HTTP requests to back end. react-router-dom is the package name for the latest version of React Router.

We also need to install type definitions for our libraries by running:

npm i --save-dev @types/react @types/react-router-dom @types/react-bootstrap @types/react-dom @types/yup

In tsconfig.json, we replace the existing code with:

  "compilerOptions": {
    "target": "es5",
    "lib": [
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react",
    "noImplicitThis": false,
    "experimentalDecorators": true
  "include": [

This disables noImplicitThis so we can use bind(this) in our components and add experimentalDecorators to use decorators needed by MobX.

Building the React App

We create some custom types for the code we will write. Create an interfaces.ts file in the src folder and add:

import { ContactsStore } from './store';

export interface Contact {
    firstName: string;
    lastName: string;
    address: string;
    city: string;
    region: string;
    postalCode: string;
    phone: string;
    email: string;
    age: string;
    id: number;
    country: string;

export interface HomePageProps {
    contactsStore: ContactsStore

export interface ContactFormProps {
    edit?: boolean,
    onSave?: any,
    contact?: Contact,
    onCancelAdd?: any,
    onCancelEdit?: any,
    contactsStore: ContactsStore

This will be the types for our data and props.

Now that we have all the libraries installed, we can start building the app. All files will be in the src folder unless mentioned otherwise. First we work on the MobX store. We create a file called store.tsx in the src folder and add the following:

import { observable } from "mobx";
import { Contact } from "./interfaces";

class ContactsStore {
    [@observable]( "Twitter profile for @observable") contacts: Contact[] = [];

    setContacts(contacts: Contact[]) {
        this.contacts = contacts;

export { ContactsStore };

This is a simple store that holds the contacts. The contacts array is where we store the contacts for the whole app. The setContacts function lets us set contacts from any component where we pass in this store object to.

This block:

ContactsStore = decorate(ContactsStore, {
  contacts: observable,
  setContacts: action,

designates the contacts array in ContactsStore as the entity that can be watched by components for changes. The setContacts is the function that can be used to set the contacts array in the store.

In App.tsx, we replace the existing code with the following:

import React from "react";
import { Router, Route } from "react-router-dom";
import HomePage from "./HomePage";
import { createBrowserHistory as createHistory } from "history";
import Navbar from "react-bootstrap/Navbar";
import Nav from "react-bootstrap/Nav";
import "./App.css";
const history = createHistory();

function App({ contactsStore }: any) {
  return (
    <div className="App">
      <Router history={history}>
        <Navbar bg="primary" expand="lg" variant="dark">
          <Navbar.Brand href="#home">Address Book App</Navbar.Brand>
          <Navbar.Toggle aria-controls="basic-navbar-nav" />
          <Navbar.Collapse id="basic-navbar-nav">
            <Nav className="mr-auto">
              <Nav.Link href="/">Home</Nav.Link>
          component={(props: any) => (
            <HomePage {...props} contactsStore={contactsStore} />

export default App;

We pass the store into any component that needs it, like the HomePage. It will then pass the component to the ContactForm.

This is where we add the navigation bar and show our routes handled by React Router. In App.css, we replace the existing code with:

.App {
  text-align: center;

Next we build our contact form. This is the most logic heavy part of our app.

In this ContactForm.tsx, we have the React.SFC<ContactFormProps> annotation for the ContactForm component so we get type checking for our component. The ContactFormProps lets us check the data type of our props.

Create a file called ContactForm.tsx and add:

import React from "react";
import { Formik } from "formik";
import Form from "react-bootstrap/Form";
import Col from "react-bootstrap/Col";
import InputGroup from "react-bootstrap/InputGroup";
import Button from "react-bootstrap/Button";
import * as yup from "yup";
import { COUNTRIES } from "./exports";
import { addContact, editContact, getContacts } from "./requests";
import { FormControl } from "react-bootstrap";
import { Contact, ContactFormProps } from "./interfaces";

const schema = yup.object({
  firstName: yup.string().required("First name is required"),
  lastName: yup.string().required("Last name is required"),
  address: yup.string().required("Address is required"),
  city: yup.string().required("City is required"),
  region: yup.string().required("Region is required"),
  country: yup
    .required("Country is required")
  postalCode: yup
    .when("country", {
      is: "United States",
      then: yup
        .matches(/^[0-9]{5}(?:-[0-9]{4})?$/, "Invalid postal code")
    .when("country", {
      is: "Canada",
      then: yup
        .matches(/^[A-Za-z]d[A-Za-z][ -]?d[A-Za-z]d$/, "Invalid postal code")
  phone: yup
    .when("country", {
      is: country => ["United States", "Canada"].includes(country),
      then: yup
        .matches(/^[2-9]d{2}[2-9]d{2}d{4}$/, "Invalid phone nunber")
  email: yup
    .email("Invalid email")
    .required("Email is required"),
  age: yup
    .required("Age is required")
    .min(0, "Minimum age is 0")
    .max(200, "Maximum age is 200")

const ContactForm: React.SFC<ContactFormProps> = ({
}: ContactFormProps) => {
  const handleSubmit = async (evt: Contact) => {
    const isValid = await schema.validate(evt);
    if (!isValid) {
    if (!edit) {
      await addContact(evt);
    } else {
      await editContact(evt);
    const response = await getContacts();

  return (
        initialValues={(contact || {}) as any}
        }: any) => (
          <Form noValidate onSubmit={handleSubmit}>
              <Form.Group as={Col} md="12" controlId="firstName">
                <Form.Label>First name</Form.Label>
                  placeholder="First Name"
                  value={values.firstName || ""}
                  isInvalid={touched.firstName && errors.firstName}
                <Form.Control.Feedback type="invalid">
              <Form.Group as={Col} md="12" controlId="lastName">
                <Form.Label>Last name</Form.Label>
                  placeholder="Last Name"
                  value={values.lastName || ""}
                  isInvalid={touched.firstName && errors.lastName}

                <Form.Control.Feedback type="invalid">
              <Form.Group as={Col} md="12" controlId="address">
                    value={values.address || ""}
                    isInvalid={touched.address && errors.address}
                  <Form.Control.Feedback type="invalid">
              <Form.Group as={Col} md="12" controlId="city">
                  value={ || ""}
                  isInvalid={ &&}

                <Form.Control.Feedback type="invalid">
              <Form.Group as={Col} md="12" controlId="region">
                  value={values.region || ""}
                  isInvalid={touched.region && errors.region}
                <Form.Control.Feedback type="invalid">

              <Form.Group as={Col} md="12" controlId="country">
                  value={ || ""}
                  isInvalid={touched.region &&}
                  { => (
                    <option key={c} value={c}>
                <Form.Control.Feedback type="invalid">

              <Form.Group as={Col} md="12" controlId="postalCode">
                <Form.Label>Postal Code</Form.Label>
                  placeholder="Postal Code"
                  value={values.postalCode || ""}
                  isInvalid={touched.postalCode && errors.postalCode}

                <Form.Control.Feedback type="invalid">

              <Form.Group as={Col} md="12" controlId="phone">
                  value={ || ""}
                  isInvalid={ &&}

                <Form.Control.Feedback type="invalid">

              <Form.Group as={Col} md="12" controlId="email">
                  value={ || ""}
                  isInvalid={ &&}

                <Form.Control.Feedback type="invalid">

              <Form.Group as={Col} md="12" controlId="age">
                  value={values.age || ""}
                  isInvalid={touched.age && errors.age}

                <Form.Control.Feedback type="invalid">
            <Button type="submit" style={{ marginRight: "10px" }}>
            {edit ? (
              <Button type="button" onClick={onCancelEdit}>
            ) : (
              <Button type="button" onClick={onCancelAdd}>

export default ContactForm;

We pass in the contactsStore from the HomePage component, allowing us to use the data and functions in contactsStore.

For form validation, we use Formik to facilitate building our contact form here, with our Boostrap Form component nested in the Formik component so that we can use Formik’s handleChange, handleSubmit, values, touched and errors parameters.

handleChange is a function that lets us update the form field data from the inputs without writing the code ourselves. handleSubmit is the function that we passed into the onSubmit handler of the Formik component. The parameter in the function is the data we entered, with the field name as the key, as defined by the name attribute of each field and the value of each field as the value of those keys. Notice that in each value prop, we have ||'' so we do not get undefined values and prevent uncontrolled form warnings from getting triggered.

To display form validation messages, we have to pass in the isInvalid prop to each Form.Control component. The schema object is what Formik will check against for form validation. The argument in the required function is the validation error message. The second argument of the matches, min, and max functions are also validation messages.

The parameter of the ContactForm function are props, which we will pass in from the HomePage component that we will build later. The handleSubmit function checks if the data is valid, then if it is then it will proceed to saving according to whether it is adding or editing a contact. Then when saving is successful we set the contacts in the store and call onSave prop, which is a function to close the modal the form is in. The modal will be defined in the home page.

Next we create a file called exports.ts, and put:

export const COUNTRIES = [
  "Antigua &amp; Barbuda",
  "Bosnia &amp; Herzegovina",
  "British Virgin Islands",
  "Burkina Faso",
  "Cape Verde",
  "Cayman Islands",
  "Cook Islands",
  "Costa Rica",
  "Cote D Ivoire",
  "Cruise Ship",
  "Czech Republic",
  "Dominican Republic",
  "El Salvador",
  "Equatorial Guinea",
  "Falkland Islands",
  "Faroe Islands",
  "French Polynesia",
  "French West Indies",
  "Guinea Bissau",
  "Hong Kong",
  "Isle of Man",
  "Kyrgyz Republic",
  "Netherlands Antilles",
  "New Caledonia",
  "New Zealand",
  "Papua New Guinea",
  "Puerto Rico",
  "Saint Pierre &amp; Miquelon",
  "San Marino",
  "Saudi Arabia",
  "Sierra Leone",
  "South Africa",
  "South Korea",
  "Sri Lanka",
  "St Kitts &amp; Nevis",
  "St Lucia",
  "St Vincent",
  "St. Lucia",
  "Timor L'Este",
  "Trinidad &amp; Tobago",
  "Turks &amp; Caicos",
  "United Arab Emirates",
  "United Kingdom",
  "United States",
  "United States Minor Outlying Islands",
  "Virgin Islands (US)",

These are countries for the countries field in the form.

In index.tsx, replace the existing code with:

import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import * as serviceWorker from "./serviceWorker";
import { ContactsStore } from "./store";
const contactsStore = new ContactsStore();

  <App contactsStore={contactsStore} />,

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers:

This passes our MobX store into our App component.

In HomePage.tsx, we put:

import React from "react";
import { useState, useEffect } from "react";
import Table from "react-bootstrap/Table";
import ButtonToolbar from "react-bootstrap/ButtonToolbar";
import Button from "react-bootstrap/Button";
import Modal from "react-bootstrap/Modal";
import ContactForm from "./ContactForm";
import "./HomePage.css";
import { getContacts, deleteContact } from "./requests";
import { observer } from "mobx-react";
import { Contact, HomePageProps } from "./interfaces";

const HomePage: React.SFC<HomePageProps> = ({
}: HomePageProps) => {
  const [openAddModal, setOpenAddModal] = useState(false);
  const [openEditModal, setOpenEditModal] = useState(false);
  const [initialized, setInitialized] = useState(false);
  const [selectedContact, setSelectedContact] = useState({});

  const openModal = () => {

  const closeModal = () => {

  const cancelAddModal = () => {

  const editContact = (contact: Contact) => {

  const cancelEditModal = () => {

  const getData = async () => {
    const response = await getContacts();

  const deleteSelectedContact = async (id: number) => {
    await deleteContact(id);

  useEffect(() => {
    if (!initialized) {

  return (
    <div className="home-page">
      <Modal show={openAddModal} onHide={closeModal}>
        <Modal.Header closeButton>
          <Modal.Title>Add Contact</Modal.Title>
            onSave={closeModal.bind(this) as any}
            onCancelAdd={cancelAddModal as any}

      <Modal show={openEditModal} onHide={closeModal}>
        <Modal.Header closeButton>
          <Modal.Title>Edit Contact</Modal.Title>
            contact={selectedContact as Contact}
      <ButtonToolbar onClick={openModal}>
        <Button variant="outline-primary">Add Contact</Button>
      <br />
      <Table striped bordered hover>
            <th>First Name</th>
            <th>Last Name</th>
            <th>Postal Code</th>
          { any) => (
            <tr key={}>
                  onClick={editContact.bind(this, c)}
export default observer(HomePage);

Notice that we have export default observer(HomePage); instead of export default HomePage;. We need to wrap HomePage with the observer function call so that the latest data from the store will be propagated into this component.

We have the React.SFC<HomePageProps> annotation for the HomePage component so we get type checking for our component. The HomePageProps lets us check the data type of our props.

It has the table for displaying the contacts and buttons to add, edit, and delete a contact. It gets data once on the first load with the getData function called in the useEffect’s callback function. useEffect’s callback is called on every render so we want to set a initialized flag and check that it loads only if it’s true.

Note that we pass in all the props of this component to the ContactForm component. To pass an argument for the onClick handler function, we have to call bind on the function and pass in the argument for the function as a second argument to bind. For example, in this file, we have editContact.bind(this, c), where c is the contact object. The editContact function is defined as follows:

const editContact = (contact) => {

c is the contact parameter we pass in.

Next we create a file called HomePage.css and put:

.home-page {
  padding: 20px;

In index.tsx, we replace the existing code with:

import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import * as serviceWorker from "./serviceWorker";
import { ContactsStore } from "./store";
const contactsStore = new ContactsStore();

  <App contactsStore={contactsStore} />,

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers:

Then we make a file called requests.tsx and add:

import { Contact } from "./interfaces";

const APIURL = 'http://localhost:3000';
const axios = require('axios');

export const getContacts = () => axios.get(`${APIURL}/contacts`);

export const addContact = (data: Contact) =>`${APIURL}/contacts`, data);

export const editContact = (data: Contact) => axios.put(`${APIURL}/contacts/${}`, data);

export const deleteContact = (id: number) => axios.delete(`${APIURL}/contacts/${id}`);

These are functions are making our HTTP requests to the back end to save and delete contacts.

Finally, in public/index.html, we replace the existing code with:

<!DOCTYPE html>
<html lang="en">

  <meta charset="utf-8" />
  <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <meta name="theme-color" content="#000000" />
  <meta name="description" content="Web site created using create-react-app" />
  <link rel="apple-touch-icon" href="logo192.png" />
  <link rel="manifest" crossorigin="use-credentials" href="%PUBLIC_URL%/manifest.json" />

      manifest.json provides metadata used when your web app is installed on a
      user's mobile device or desktop. See
      Notice the use of %PUBLIC_URL% in the tags above.
      It will be replaced with the URL of the `public` folder during the build.
      Only files inside the `public` folder can be referenced from the HTML.

      Unlike  "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
      work correctly both with client-side routing and a non-root public URL.
      Learn how to configure a non-root public URL by running `npm run build`.
  <title>React Address Book App</title>
  <link rel="stylesheet" href=""
    integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous" />

  <noscript>You need to enable JavaScript to run this app.</noscript>
  <div id="root"></div>
      This HTML file is a template.
      If you open it directly in the browser, you will see an empty page.

You can add webfonts, meta tags, or analytics to this file.
      The build step will place the bundled scripts into the <body> tag.

To begin the development, run `npm start` or `yarn start`.
      To create a production bundle, use `npm run build` or `yarn build`.


This changes the title and add the Bootstrap stylesheet.

Now we can run the app by running set PORT=3001 && react-scripts start on Windows or PORT=3006 react-scripts start on Linux.

To start back end, we first install the json-server package by running npm i json-server. Them go to our project folder and run:

json-server --watch db.json

In db.json, change the text to:

  "contacts": [

Now we have the contacts endpoints defined in requests.js available.

Leave a Reply

Your email address will not be published. Required fields are marked *