Juan Cruz Martinez I'm an entrepreneur, developer, author, speaker, YouTuber, and doer of things.

TypeScript, React, and create-react-app: Leveraging the power of Types

9 min read 2528

TypeScript Logo

JavaScript is a fantastic programming language that is extending its scope beyond browsers into APIs, mobile development, desktop applications, and much more. But with all of its powerful features and rising potential, JavaScript is missing a crucial feature that helps with code organization and efficiency: types.

As an example, let’s take a look at the following code and analyze it:

let someText = "Hello World!";
someText = 123;
someText = [123, 'hello', { isThisCorrect: true }]

Although coding this in most programming languages wouldn’t work, JavaScript allows for it. We can create a variable as a string, then assign a number, object, or even an array to it. But this has its pros and cons.

As applications grow, systems become more complex, and dependencies in projects increase, it can be difficult for developers to understand the variables, properties, and methods in their full context—particularly when working with frameworks and external dependencies.

Wouldn’t it be nice if, while programming, our IDE could suggest and autocomplete the properties of an object while typing, even if this object is the result of a remote fetch operation? Or that we could inspect a variable and easily see all its methods?

This is precisely the problem that TypeScript resolves by adding Types to variables.

Now, to see TypeScript in action, let’s develop a simple application using the React framework and create-react-app (CRA) with TypeScript support.

Setting up a new project with TypeScript

To begin, let’s set up our app in TypeScript and pre-configure webpack, Babel, and our dependences using create-react-app with the following command:

npx create-react-app my-app --template typescript

To start the project, run:

cd my-app
npm start

Notice that the site looks similar to a JavaScript version. However, when we start looking at the code, you’ll notice a few differences.

We made a custom demo for .
No really. Click here to check it out.

The TypeScript compiler

Let’s take a closer look at the components and tests starting with the file App.tsx. The first thing you may have noticed is its file extension. All component files are now tsx instead of jsx, and, likewise, files like setupTests.js are now setupTests.ts. But why?

Currently, browsers and Node don’t offer direct support for TypeScript; they only understand JavaScript, and because of that, we need to convert our TypeScript files into JavaScript files. The TypeScript compiler is responsible for performing this task.

Because the compiler needs to know which files to compile versus files that are pure JavaScript, different file extensions, like the ones mentioned above, are used.

Typing variables in TypeScript

JavaScript is not typed, meaning that you can’t assign a data type to a variable, but JavaScript still understands the difference between a number, a string, or an object. JavaScript will do its best at runtime to infer the type of the variable by the value it contains.

Let’s take a look at the following example:

let helloWorld = "Hello World";

Because the value contained in the variable helloWorld is a string, that variable is now of type string, and you can perform any operation you normally would on a string. However, if that variable changes at a later point, the type of the variable will change.

TypeScript will take this approach and assign a variable its type depending on its value at the moment of declaration, so the same line of code will work on TypeScript. The difference will rely on what happens when you want to assign a new value of a different type to the variable.

We saw already that JavaScript would allow you to do it, but what would happen if we try the same with TypeScript?

let helloWorld = "Hello World";
helloWorld = 10;

Interestingly enough, our IDE (VS Code, WebStorm, or any other with support for TypeScript) already detected an issue, highlighted the variable in red, and gave us information about it.

Highlighted Errors in TypeScript
Example of how WebStorm highlights errors in Types when using TypeScript.

Moreover, the compilation process will result in errors, and you won’t be able to proceed with running your application:

TypeScript Prevents App Building With Too Many Errors
TypeScript prevents the app from building if there are errors in Types.

So far, we let TypeScript decide on the variable data type; this is called type by inference. However, on other occasions, we may want to manually assign a type to a variable, perhaps because we still don’t have its value or because the variable could have more than one type, though more on that later.

Let’s now declare a variable using explicit types:

let helloWorld:string  = "Hello World";

Easily enough, we can pass the type during declaration right after the variable name.

React <3 TypeScript

Even though the default project generator for React works with JavaScript, React and TypeScript are made for each other. Building and utilizing components is much cleaner with TypeScript, as we will see next.

It’s time we talk about our app. It is a simple screen that allows users to search repositories in GitHub using their search API. Demo of the app here:

App Demo

And you can check out the full code from GitHub.

Remote data types

Because we need to fetch data from our API, we would need an object to store it, and that object needs to be mapped to a type. So let’s start defining our API data types by analyzing the response and building a custom type.

Here is a sample response for referencing:

{
  "total_count": 40,
  "incomplete_results": false,
  "items": [
    {
      "id": 3081286,
      "node_id": "MDEwOlJlcG9zaXRvcnkzMDgxMjg2",
      "name": "Tetris",
      "full_name": "dtrupenn/Tetris",
      "owner": {
        "login": "dtrupenn",
        "id": 872147,
        "node_id": "MDQ6VXNlcjg3MjE0Nw==",
        "avatar_url": "https://secure.gravatar.com/avatar/e7956084e75f239de85d3a31bc172ace?d=https://a248.e.akamai.net/assets.github.com%2Fimages%2Fgravatars%2Fgravatar-user-420.png",
        "gravatar_id": "",
        "url": "https://api.github.com/users/dtrupenn",
        "received_events_url": "https://api.github.com/users/dtrupenn/received_events",
        "type": "User"
      },
      "private": false,
      "html_url": "https://github.com/dtrupenn/Tetris",
      "description": "A C implementation of Tetris using Pennsim through LC4",
      "fork": false,
      "url": "https://api.github.com/repos/dtrupenn/Tetris",
      "created_at": "2012-01-01T00:31:50Z",
      "updated_at": "2013-01-05T17:58:47Z",
      "pushed_at": "2012-01-01T00:37:02Z",
      "homepage": "",
      "size": 524,
      "stargazers_count": 1,
      "watchers_count": 1,
      "language": "Assembly",
      "forks_count": 0,
      "open_issues_count": 0,
      "master_branch": "master",
      "default_branch": "master",
      "score": 1.0
    }
  ]
}

Because our application is very simple, we won’t need to map all those fields, but let’s see how it would work. Let’s create a file (and folder) under src named Types/GitHub.ts.

In this file, we will define a custom data type to represent the JSON response. TypeScript has two main options for defining types: interface and types.

Each has its properties and rules, and they are covered in full in Types vs Interfaces in TypeScript, so we won’t go into much detail here, but as a general rule, you may want to use interface for props and types to represent objects.

In our case, we are representing an object, so we will use the types syntax to declare our GitHubSearchResultType, which is composed of the total_number of repositories found (number), an incomplete_results flag (true or false) and a list of items (repositories).

For the first two properties, it’s very simple: each corresponds to a TypeScript basic type so we can reference immediately:

export type GitHubSearchResultType = {
    total_count: number;
    incomplete_results: boolean;
}

Now, when it comes to the items properties, the situation is a bit more complicated. We know it’s an array of objects, so we could use the keyword any of TypeScript, which defaults the variable to the JavaScript behavior ignoring Types. It would look something like this:

export type GitHubSearchResultType = {
    total_count: number;
    incomplete_results: boolean;
    Items: Array<any>;
}

Note that the syntax for defining the array is a bit different, as we have two types in one definition: the main type Array and the type of the elements of the array which go in between <>.

This works, but it’s not ideal, and in most projects, the use of any is forbidden (and monitored by lint rules) because you would be ignoring the reason you are using TypeScript to begin with.

A better way to do it is to define a second custom type and map the array to this type, something like:

typescript
export type GitHubSearchResultType = {
    total_count: number;
    incomplete_results: boolean;
    items: Array<GitHubRepository>
}

Which is great! It’s clear what items are and what properties each item would have. We are now only missing the definition for GitHubRepository.

export type GitHubRepository = {
    id: string;
    full_name: string;
    html_url: string;
}

Building component: listing repositories

It’s time to start building components, and we are going to start with a very simple component that will receive a list of repositories and draw them on the screen. We will name this component ListRepositories.tsx and store it in the folder src/Components.

Components in React with TypeScript have two main differences with JavaScript React components.

  1. You don’t need PropTypes for type checking as we have TypeScript
  2. The component declaration is slightly different

Let’s start by understanding how to define props with TypeScript. Our component needs access to the list of repositories that we will receive by props, so let’s define that by using the interface syntax.

interface Props {
    repositories?: Array<GitHubRepository>;
}

Interfaces very similar to types define the objects’ properties in the body between the brackets, and the properties are defined in the same way.

In our particular example, we introduced a new concept called optional properties (noted by adding a ? right after the variable name). These optional properties can be undefined, null, or a value of the given type.

The interface we created was named Props. Still, it could be anything, like ListRepositoryProps or ListRepositoryPropsType.

Thus we must tell React what name was given to the props type when we declare our component, and we achieve that by using React.FC as follows:

const ListRepositories: React.FC<Props> = (props) => {

In that one line, we created a functional component and told react that our props are of Props type. And because it’s a functional component, we can do cool things like object destructuring. Our modified declaration now looks like:

const ListRepositories: React.FC<Props> = ({ repositories = [] }) => {

If you wonder why we set the repositories property to optional if we always require a value, it is because the parent of this component may pass an undefined, and if that’s the case, we can assume an empty list.

Here is the full code for the component:

import React from 'react';
import {GitHubRepository} from "../Types/GitHub";

interface Props {
    repositories?: Array<GitHubRepository>;
}

const ListRepositories: React.FC<Props> = ({ repositories = [] }) => {
    return (
        <ul>
            {repositories.map(repository => (
                <li key={repository.id}>
                   <a href={repository.html_url} target="_blank">{repository.full_name}</a>
                </li>
            ))}
        </ul>
    );
}

export default ListRepositories;

Creating the search form

Next in our list is to build a new component responsible for capturing the user input and initiating the search operation. Since we are already pros at React and TypeScript, I will present the full code for the form and then talk about the critical elements below.

import React from 'react';

interface Props {
    search(query: string): void;
}

const SearchForm: React.FC<Props> = (props) => {
    function handleSubmit(e: React.FormEvent) {
        e.preventDefault();
        const formData = new FormData(e.target as HTMLFormElement);
        props.search((formData.get('query') || '') as string);
    }

    return (
        <form onSubmit={handleSubmit}>
            <label>
                Query:
                <br />
                <input type="text" name="query" />
            </label>
            <button>Search</button>
        </form>
    );
}

export default SearchForm;

Like our previous component, we are using an interface to define our props; however, we don’t have properties but a method instead.

TypeScript allows us to define function types, and we can do that by specifying a function name, its params, and types and its return type. It also has support for multiple syntaxes like:

functionName(param1: type, …): type;

Or by using arrow functions:

functionName: (param1: type, ...) => type;

Examples:

interface Props {
    search(query: string): void;
}

interface Props {
    search: (query: string) => void;
}

In this form, we have another special use of TypeScript, which is casting, and we can see it here:

props.search((formData.get('query') || '') as string);

Why do we need to cast? The function formData.get is a union type, meaning that it can be one of the multiple types—in this case string, File, or undefined. We know that for our purpose, the only option is to be a string, and thus, we cast.

API fetching and putting it all together

We have all our pieces ready. Now we need to combine them on the App.tsx component and add the functionality to fetch the remote data from the GitHub API.

Before we reveal the code, it’s important to clarify that we are using axios to fetch the data from the API. You can install axios typing:

npm install axios

Alright, now it’s time for the code:

import React from 'react';
import axios from 'axios';
import {GitHubRepository, GitHubSearchResultType} from "./Types/GitHub";
import SearchForm from "./Components/SearchForm";
import ListRepositories from "./Components/ListRepositories";

function App() {
    const [repositories, setRepositories] = React.useState<Array<GitHubRepository>>();

    // Performs the search
    async function search(query: string) {
         const result = await axios.get<GitHubSearchResultType>(`https://api.github.com/search/repositories?q=${query}`);
         setRepositories(result.data.items);
    }

    return (
        <div>
            <SearchForm search={search}/>
            <ListRepositories repositories={repositories}/>
        </div>
    );
}

export default App;

It’s all pretty much standard React code except for two instances, which use TypeScript Generics.

Generics allow for definitions to have “blank” types completed at a later time in code. A perfect example of this in React is the function React.useState.

useState will define a getter and setter that we can use in our code to store information into React’s state. But how would React know the type of this getter and setter?

The answer is generics. In essence, a function like useState would be declared as:

function useState<S = undefined>(): [S | undefined, Dispatch<SetStateAction<S | undefined>>];

Where S can be of any type and it’s specified during the function call as follows:

const [repositories, setRepositories] = React.useState<Array<GitHubRepository>>();

Or by using implicit type designation:

const [someText, setSomeText] = React.useState('Hello World!');

The second instance using generics is when we use axios.

const result = await axios.get<GitHubSearchResultType>(`https://api.github.com/search/repositories?q=${query}`);
         setRepositories(result.data.items);

In this case, the function get will return the JSON response data mapped into the given type.

Conclusion

In this tutorial, we learned how to leverage the power of TypeScript to build scalable and maintainable React applications. I hope you enjoyed reading this article as much as I enjoyed writing it, and as I always say, once you go TypeScript, you never go back!

Thanks for reading!

Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — .

Writing a lot of TypeScript? Watch the recording of our recent TypeScript meetup to learn about writing more readable code.

TypeScript brings type safety to JavaScript. There can be a tension between type safety and readable code. Watch the recording for a deep dive on some new features of TypeScript 4.4.

Juan Cruz Martinez I'm an entrepreneur, developer, author, speaker, YouTuber, and doer of things.

Leave a Reply