Back

Using Typescript in React: a Crash Course

Using Typescript in React: a Crash Course

Typescript is a strict superset of the Javascript programming language, extending Javascript with new features and syntax.

Typescript is like a built-in unit test and gives us the benefit of the compiler watching our back, prompting errors even before the code is run, and adding static and robust typing to our applications.

This article is a quick guide to get you started and comfortable using Typescript in React, creating simple use cases with Typescript. You’ll need an understanding of React, familiarity with Javascript Types, and basic understanding of Node and npm.

What is Typescript?

Typescript is an open-source programming language created by Microsoft, and it was designed to develop large applications by adding static typing controls to JavaScript. It may be used to develop applications for both client-side (web-browser) and server-side (Node.js or Deno).

In simple terms, Typescript is Javascript plus static typing features whenever you want them. The idea of having static type checks in Javascript is amazing; that is one reason why the use of Typescript is rapidly increasing.

Why do Software Companies prefer to use Typescript?

The use of Typescript has soared compared to the last six years ago, according to several rankings like this, this accounts.

Using Typescript (instead of Javascript) makes it possible to remove certain software anomalies earlier during the development phase and secure the code before going into production.

Typescript also makes it easier to reuse code and manage types and special cases, which the development team would not have thought of easily. It forces the team to take these topics into account, and as a result, best practices are encouraged, saving development time and reducing the frequency of bugs.

Learning Typescript is worthwhile when a team is more concerned about security. Typescript type checks codes at compile time, detecting possible issues before they become vulnerabilities, making it easier to trace application errors or weaknesses.

Adding Typescript to React

To create a new React project with Typescript template append
--template typescript to the npx create-react-app command. Run this on the terminal; it creates a new react app called my-app.

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

Using this command, the directory structure can be viewed as:

Directory structure

Run npm start on the terminal, to test that everything is working.

How does TypeScript compile React code?

Open the project directory in your preferred editor (I’m using VSCode here). You will notice a tsconfig.json file. This is where the magic of Typescript happens. Typescript looks for the tsconfig.json file in the project root folder, and that file provides configuration options for the compiler.

Typescript compiles our React code to type-check it. The v8-engine, which is the program that executes Javascript in the browser, does not understand Typescript, so transpilation is needed.

To run our app, Typescript is converted into plain Javascript by the Typescript compiler (tsc). The compiler goes through the src directory in our project and converts all .ts files into .js files. By default, it ignores files not in the src directory, but this default configuration can be modified in the tsconfig.json file.

Understanding Types

Before we delve into building React applications with Typescript, let’s talk about an essential part of Typescript, which enables its static type checking abilities. Generally, programming languages fall into two categories: statically typed or dynamically typed. In static typing, types of variables must be known at compile time. If a variable is declared, the compiler must know the variable type. Some languages in this category are C# and Java. In dynamic typing, the type of a variable is known only when the program is running. Python and JavaScript are languages in this category. Using Typescript converts JavaScript into a statically typed language.

What are types? They are labels that describe the different properties and functions a value has. Javascript has a number of built-in (primitive) types, all accessible to Typescript: string, number, boolean, null, undefined, etc. Typescript also has some advanced types:

  • void: denotes the absence of any type.
  • empty type : describes an object that has no property of its own.
  • tuple: works like an array, but the number of elements here is fixed. The types of elements in a tuple are known and can be of different types.
  • enum: is a group of constant values that are closely related and known.
  • any: allows us to assign the value of any type to a variable. Used when a value type is unknown
  • never: is a type that contains no value, so we can’t assign any value to a variable with a never type.
  • union: this describes a value that can be one of several types number | string | boolean
  • object type: To define an object type we list its properties and their types: {x:number, y:number}

Type annotations

Typescript uses its type annotation to specify the type of a value (string, boolean, number, etc). Typescript uses the syntax : type after a value’s identifier, where type is a valid type such as string, boolean, number, etc.

let variableName: type;

let name: string= 'Jane';
const active: boolean= true;
let arrayName: number[] = [2, 4, 6, 8]

Once a value identifier is annotated with a type, it can only be used as that type. If the identifier is used with a different type, tsc will issue an error.

Type Interfaces

Typescript interfaces are essential to enforcing a particular object shape. An interface is like an object; it contains information about the object’s properties and types. An interface explicitly tells the Typescript compiler about the property names and the value types an object can have.

interface Location{
 from: string;
 to: string;
}

function printCoord(holiday: Location) {
    console.log("We are going to" + from + "to" + to);
}

printCoord({from: "london", to: "new-york"});

After defining the Location interface, we can use it as a type. Here we annotate it to the function parameter to type-check the function arguments when called.

There are several ways typescript interface can provide type information in our codes.

Type Aliases

Aliases allow us to create a new name for an existing type. Here a new type is not defined; we only create a new name for the type. It is common to use the same type more than once, so a type alias is a convenient way to give a new name to an object type declaration.

Unlike an interface, type alias can also be used for other types such as primitives, unions, strings, and tuples.

Type aliases and interfaces are somewhat similar. Type interfaces work better with objects and method objects, while Type aliases work better with functions, complex types, etc.

For creating new type aliases, we use the type keyword.

type Location= {
 from: string;
 to: string;
}

function printCoord(holiday: Location) {
    console.log("We are going to" + from + "to" + to);
}

printCoord({from: "london", to: "new-york"});

Generics

Generics help make the type system flexible and reusable. They allow us to generically define types that a function can return with, using type-variables such as <T>. T is a placeholder and can be changed to any other variable name.

Why do we need Generics? Let’s take a look at a regular typescript function without generics.

 function removeItemFromArray(
    arr: Array<number>,
    item: number
  ): Array<number> {
    const index = arr.findIndex((i) => i === item);
    arr.splice(index, 1);
    return arr;
  }

   console.log(removeItemFromArray([1, 2, 3], 2))         //  [1, 3]

Above, we defined a function that removes a specified number from an array when called. Assuming we need to remove a string from the Array, we would have to develop a new function.

  function removeItemFromArray(
    arr: Array<string>,
    item: string
  ): Array<string> {
    const index = arr.findIndex((i) => i === item);
    arr.splice(index, 1);
    return arr;
  }

 console.log(removeItemFromArray(["red", "green", "blue"], "green"));      //  ['red', 'blue']

The logic for both functions is the same. Later we may decide to remove another value type (boolean, object…) from an array, and we’d need yet more versions of the same logic… but creating a new function each time we want to remove a value from an array doesn’t scale.

One solution is to use the ‘any’ type; to do this, we need to set the type of the array argument to any[]. But the drawback here is that any doesn’t enforce the type of the returned element, which means it is not type-safe. A better solution to avoid creating multiple functions is to use Generics in typescript.

Using generic types to create a new removeItemFromArray() function would be as follows:

  function removeItemFromArray<T>(arr: Array<T>, item: T): Array<T> {
    const index = arr.findIndex((i) => i === item);
    arr.splice(index, 1);
    return arr;
  }

  console.log(removeItemFromArray<string>(["red", "green", "blue"], "green"));
  console.log(removeItemFromArray<number>([1, 2, 3, 4, 5, 6], 3));

Here we can notice the function uses the type variable T. T lets us capture the type provided when the function is called. The function also uses the type variable ‘T’ as its return type.

Now our function can be used with different types. In the above example, we can call the function explicitly by passing a type(number, string, etc.) as generic to the removeItemFromArray() function.

Conditional Types

Conditional type helps describe the relationship between types by using the extend keyword and selecting one of two possible types. Conditional types look like the ternary operator in Javascript (condition ? trueExpression : falseExpression ). They work great with generics.

someType extend anotherType ? trueType : falseType;

The syntax someType extends anotherType describes the type relationship between them. If this condition is met, the trueType is selected; else, the falseType is selected.

Example for Conditional Types:


interface Animal {
  adopt() : void;
}

interface Dog extends Animal {
  woof() : void;
}

interface Cat {
  meow() : void;
}

type Owner1 = Dog extends Animal ? boolean : string;    // returns boolean type

type Owner2 = Cat extends Animal ? boolean : string;    // returns string type

Here Dog extends Animal; therefore, it returns a boolean type, while the Cat interface is not extended to Animal. So for Owner2, a string is returned.

Functional components

A React Functional Component is a function that takes props objects and returns JSX elements. In React components, we need to consider the type of props coming in. We will be writing all code in the App.tsx file to make things simple to follow. Delete everything inside the function in that file and return the simple code below.

import React from "react";
import "./App.css";

function App() {
  return <h1>Hello World</h1>;
}

//add "Employee" component .....

export default App;

There are several ways of creating a functional component in React using Typescript.

Props Type as an Argument

This is one of the easiest ways to declare a functional component. First, we state the type using the interface or (type) keyword. Then annotate the interface object to the function props. The interface object explicitly states the props type and name.

Introduce a new component, Employee, and call it inside our App component above it.

import React from "react";
import "./App.css";

//type declaration
interface MessageProps {
  message: string;
}

function App() {
  return (
    <>
      <Employee message="Hello" />
    </>
  );
}

// "Employee" component 
function Employee({ message }: MessageProps) {
  return (
    <>
      <h1>Welcome {message}</h1>
    </>
  );
}

export default App;

Inline Type Declaration

Another way to create a functional component in React using Typescript is to inline a type declaration in the function component. This eliminates naming the props type but gets uncomfortable if the function component gets complex. Change the Employee component in the previous code to:

// change "Employee" component 
function Employee({ message }: { message: string }) {
  return (
    <>
      <h1>Welcome {message}</h1>
    </>
  );
}

React.FunctionComponent or React.FC

A common way of creating a functional component in React using Typescript is to import React’s FunctionComponent or FC type; FC is just an alias for FunctionComponent. With this method, we cannot create the component using a function declaration, so we will have to use a function expression.

Change the Employee component in the previous code to:

// change "Employee" component 
const Employee: React.FC<MessageProps> = ({ message }) => {
  return (
    <>
      <h1>{message}</h1>
    </>
  );
};

Using React.FunctionComponent or React.FC approach comes with some disadvantages:

  • Imply type of children: React.FC provides access to props’ children even when we don’t want it. Our App function now accepts a children prop, even though we are not doing anything with it.
  • Type a function, not its arguments: Using React.Fc types, we will have to annotate the function type itself. This means we need to use a function expression like an anonymous function assigned to a const/variable or arrow function when using React.Fc like in the example above.
  • React.FC doesn’t fit well with Generics.

React.FC can be helpful for beginners just getting into Typescript in React as it guides us with types. But it is not generally advisable to use React.FC today because of its disadvantages.

Open Source Session Replay

OpenReplay is an open-source, session replay suite that lets you see what users do on your web app, helping you troubleshoot issues faster. OpenReplay is self-hosted for full control over your data.

replayer.png

Start enjoying your debugging experience - start using OpenReplay for free.

Types for State and Props in React

In Typescript, variables, states, and props are limited to the types they are defined with; when we write our code. These types can be defined on the variable values using type annotations with primitive types, type interface, type alias, tuples, union, etc.

import React, { useState } from "react";
import "./App.css";

interface IProps {
  staff: string;
  message: string;
}

interface IState {
  position: string;
  experience: number;
}

function App() {
  return (
    <>
      <Employee message="Good Morning" staff="Ryan" />
    </>
  );
}

function Employee({ staff, message }: IProps) {
  let [state, setState] = useState<IState>({
    position: "Software Engineer",
    experience: 17,
  });
  return (
    <div className="App">
      <h2>
        {message} {staff}
      </h2>
      <ul>
        <li>
          <span>Position:</span> {state.position}
        </li>
        <li>
          <span>Year Of Experience:</span> {state.experience}
        </li>
      </ul>
    </div>
  );
}

export default App;

Notice the use of interface to specify our state and props object types, and the type annotations used in the code. This is a typical pattern for type-checking state and prop values.

Event and Input form handling

Here is a tricky one, how do we get to know or specify the type passed to an event on change or input? React provides many built-in event types that we can use to specify the type for all events. One easy way to tell the type of event to use for a particular event is to hover on the event handler (e.g., onChange, onSubmit, onBlur).

Getting types for events

Hover on an event, and you’ll see the appropriate type for the event. Let’s see a quick example of a form using the event type in react:

import React, { useState } from "react";
import "./App.css";

type IProfile = {
  username: string;
  password: string;
};

function App() {
  const [profile, setProfile] = useState<IProfile>({
    username: "",
    password: "",
  });

  //Checking Event Type for onChange Method
  let updateInput = (event: React.ChangeEvent<HTMLInputElement>): void => {
    setProfile({
      ...profile,
      [event.target.name]: event.target.value,
    });
  };

  //Checking Event Type for onSubmit Method
  let login = (event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    console.log(profile);
  };

  return (
    <div className="App">
      <h2>Employee Login</h2>
      <form onSubmit={login}>
        <div>
          <input
            name="username"
            value={profile.username}
            onChange={updateInput}
            type="text"
            placeholder="Username"
          />
        </div>
        <div>
          <input
            name="password"
            value={profile.password}
            onChange={updateInput}
            type="password"
            placeholder="Password"
          />
        </div>
        <input type="submit" />
      </form>
    </div>
  );
}

export default App;

JSON Documents

Imagine a scenario where we are to work with a complex JSON object, like fetching an API and still having to use Typescript to type-check its data. This can be difficult; if possible, most programmers would rather avoid it.

One quick and secure way of using types for JSON data (API response) is to convert them to Typescript interfaces using tools like quicktype, transform, etc.

Getting types for JSON

To use the tools above, copy and paste the API JSON object into the tool, and it auto generates the interface type for us. Let’s see an example using quicktype.io to convert our JSON data into types for type-checking:

import React, { useEffect, useState } from "react";
import axios from "axios";
import "./App.css";

// Generated interface type from "quicktype.io"
interface User {
  userId: number;
  id: number;
  title: string;
}

function App(): JSX.Element {
  const [employees, setEmployees] = useState<User[]>([]);

  useEffect(() => {
    axios
.get("https://jsonplaceholder.typicode.com/albums")
      .then((res) => setEmployees(res.data));
  }, []);

  return (
    <div className="App">
      <h1>COB Summary</h1>
      <table>
        <thead>
          <tr>
            <th>Id</th>
            <th>Job Summary</th>
          </tr>
        </thead>
        <tbody>
          {employees.length > 0 &&
            employees.map((employee) => {
              return (
                <tr key={employee.id}>
                  <td>{employee.id}</td>
                  <td>{employee.title}</td>
                </tr>
              );
            })}
        </tbody>
      </table>
    </div>
  );
}

export default App;

Promises

Another Javascript feature extended to Typescript is the Javascript promise. In Typescript, the return type of promise is defined immediately after the promise keyword, determining the data type of the value returned by the promise when it is resolved.

Since errors can take any shape, the default data type of value returned when the promise is rejected is set to any by Typescript. Let’s look at the following example of using promises in React.

const pickEvenNumber = new Promise<number>((resolve, reject) => {
      const isEvenNumber = Math.floor(Math.random() * 20);
      if (isEvenNumber % 2 === 0) {
        resolve(isEvenNumber);
        return;
      }
      reject(isEvenNumber);
    });

    pickEvenNumber
      .then((res) => console.log("Resolved: Even Number " + res))
      .catch((err) => console.log("Rejected: Not Even Number ", err));

In the above example, pickEvenNumber is a promise created using the promise constructor that resolves a number. The resolved data type of this promise is a number; hence tsc won’t allow us to call the resolve method with any other value except a number type.

Conclusion

In this article, we used Typescript in React to create functions, states, props, promises, and much more. Understanding Typescript is definitely an ace up your sleeve in your developer career.

For more in-depth knowledge on using Typescript with React, visit the documentation. You can find the article’s source code here.