React with TypeScript: Basics and Best Practices

  • 2020-03-12 08:12 AM
  • 430

React and TypeScript are two awesome technologies used by a lot of developers these days. An updated handbook/cheat sheet for working with React.js and TypeScript.

There is no single “right” way of writing React code using TypeScript.
As with other technologies, if your code compiles and works, you probably did something right.

So, here I’m going to list some useful code-snippets that follow said “best practices”. There are a lot of them, some that you might’ve used already in the past and some that might be new. Just go through the list and make mental notes. Bookmarking this article for future reference might be a good idea as well.

By building projects using React with TS, you make sure your components are easily comprehensible to other developers (as well as to your future self). That is absolutely crucial for making them ready for sharing. It’s a great way to write maintainable code and optimize your team collaboration.

Read Also: What’s New Features of TypeScript 3.8

Getting started

create-react-app with TypeScript

$ npx create-react-app your-app-name --template typescript

If you’re more of a fan of Yarn, you can use the following command:

$ yarn create react-app your-app-name --template typescript

In either case, notice how we’re not directly using the app, rather, we’re using other tools that will download the latest version of the app whenever it’s required. This helps ensure you’re not using an outdated version.

Read Also: React-Redux with TypeScript

Basics

Some of the very interesting tidbits added by TS to the language are:

Interfaces

One of the many benefits TypeScript brings to the table, is access to constructs such as this, which allows you to define the interface of your components or even any other complex objects you might want to use with them, such as the shape your Props object will have (i.e how many properties and their types).

import React from 'react';

interface IButtonProps {
    /** The text inside the button */
    text: string,
    /** The type of button, pulled from the Enum ButtonTypes */
    type: ButtonTypes,
    /** The function to execute once the button is clicked */
    action: () => void
}

const ExtendedButton : React.FC<IButtonProps> = ({text, type, action}) => {

}

The above code ensures that whoever uses your components needs to add exactly 3 properties:

  • text: which needs to be a String
  • type: which needs to be a ButtonType option (I’ll cover Enums in a second)
  • action: which is a simple function

Read Also: 6 Front-End Challenges in 2020

Note that we “extended” the FC (Functional Component) type with our own custom interface. That gives our function all the generic functional component definitions such as the ‘children’ prop and a return type that must be assignable to JSX.Element.

If you ignore one of them or send something that’s not compatible, both the TypeScript compiler and your IDE (assuming you’re using a JavaScript specific IDE, such as Code) will notify you and won’t allow you to continue until you fix it.

A better way to define our ExtendedButton element would be to extend a native HTML button element type like so:

import React, {ButtonHTMLAttributes} from 'react';

interface IButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
    /** The text inside the button */
    text: string,
    /** The type of button, pulled from the Enum ButtonTypes */
    type: ButtonTypes,
    /** The function to execute once the button is clicked */
    action: () => void
}

const ExtendedButton : React.FC<IButtonProps> = ({text, type, action}) => {

}
const ExtendedButton : React.FC<IButtonProps> = ({text, type, action} : IButtonProps) => {

}

(The props are defined directly and explicitly using :IButtonProps in addition to defining the component with :React.FC<IButtonProps>)

Read Also: JavaScript Basics Before You Learn React

Enums

Just like with Interfaces, Enums allow you to define a set of related constants as part of a single entity.

//...
/** A set of groupped constants */
enum SelectableButtonTypes {
    Important = "important",
    Optional = "optional",
    Irrelevant = "irrelevant"
}

interface IButtonProps {
    text: string,
    /** The type of button, pulled from the Enum SelectableButtonTypes */
    type: SelectableButtonTypes,
    action: (selected: boolean) => void
}

const ExtendedSelectableButton = ({text, type, action}: IButtonProps) => {
    let [selected, setSelected]  = useState(false)

    return (<button className={"extendedSelectableButton " + type + (selected? " selected" : "")} onClick={ _ => {
        setSelected(!selected)
        action(selected)
    }}>{text}</button>)
}

/** Exporting the component AND the Enum */
export { ExtendedSelectableButton, SelectableButtonTypes}

Importing and using Enums:

import React from 'react';
import './App.css';
import {ExtendedSelectableButton, SelectableButtonTypes} from './components/ExtendedSelectableButton/ExtendedSelectableButton'

const App = () => {
  return (
    <div className="App">
      <header className="App-header">
        <ExtendedSelectableButton type={SelectableButtonTypes.Important} text="Select me!!" action={ (selected) => {
          console.log(selected) 
        }} />       
        
      </header>
    </div>
  );
}

export default App;

Please note that unlike Interfaces or Types, Enums will get translated into plain JavaScript. So, for example, this:

enum SelectableButtonTypes {Important = "important",Optional = "optional",Irrelevant = "irrelevant"}

will transform into this:

"use strict";var SelectableButtonTypes;(function (SelectableButtonTypes) {SelectableButtonTypes["Important"] = "important";SelectableButtonTypes["Optional"] = "optional";SelectableButtonTypes["Irrelevant"] = "irrelevant";})(SelectableButtonTypes || (SelectableButtonTypes = {}));

Read Also: Angular vs React vs Vue: Which one will be popular in 2020

Interfaces vs Types alias

A common question that newcomers to TypeScript have is whether they should be using Interfaces or Type Aliases for different parts of their code — after all, the official documentation is a bit unclear regarding that topic.

Truth is, although these entities are conceptually different, in practice, they are quite similar:

  1. They can both be extended.
//extending interfaces
interface PartialPointX { x: number; }
interface Point extends PartialPointX { y: number; }

//extending types
type PartialPointX = { x: number; };
type Point = PartialPointX & { y: number; };

// Interface extends type 
type PartialPointX = { x: number; };
interface Point extends PartialPointX { y: number; }

//Type alias extends interfaces
interface PartialPointX { x: number; }
type Point = PartialPointX & { y: number; };

2. They can both be used to define the shape of objects.

//defining the interface for objects
interface Point {
  x: number;
  y: number;
}

//using types as well
type Point2 = {
  x: number;
  y: number;
};

Read Also: Push Notifications in PWA Using Firebase and React

3. They both can be implemented in the same way.

//implementing the Interface
class SomePoint implements Point {
  x: 1;
  y: 2;
}

//Implementing the Type alias
class SomePoint2 implements Point2 {
  x: 1;
  y: 2;
}

type PartialPoint = { x: number; } | { y: number; };

// This is the only thing you can't do: implement a union type
class SomePartialPoint implements PartialPoint {
  x: 1;
  y: 2;
}

The only extra feature Interfaces bring to the table (that Type aliases don’t), is “declaration merging” which means you can define the same interface several times and with each definition, the properties get merged:

interface Point { x: number; } //declaration #1
interface Point { y: number; } //declaration #2

// These two declarations become:
// interface Point { x: number; y: number; }
const point: Point = { x: 1, y: 2 };

Read Also: Building Live Streaming App with Node.js and React

Optional types for your props

Part of the benefits of using Interfaces is that you’re able to enforce the properties of your props for your components. However, thanks to the optional syntax available through TypeScript, you can also define optional props, like this:


//...
interface IProps {
  prop1: string,
  prop2: number, 
  myFunction: () => void,
  prop3?: boolean //optional prop
}

//...
function MyComponent({...props}: IProps) {
  //...
}

/** You can then use them like this */
<mycomponent prop1="text here" prop2=404 myFunction={() = {
  //...
}} />

<mycomponent prop1="text here" prop2={404} myFunction={() = {
  //...
}}  prop3={false} />

Hooks

Hooks are the new mechanics React provides to interact with several of its features (such as the state) without the need to define a class.

Adding type check to hooks

Hooks such as useState receive a parameter and correctly return the state (again, that’s for this case) and a function to set it.

Thanks to TypeScript’s type validation, you can enforce the type (or interface) of the initial value of the state, like this:

const [user, setUser] = React.useState<IUser>(user);

Nullable values to hooks

However, if the initial value for your hook can potentially be a null, then the above example will fail. For these cases, TypeScript allows you to set an optional type as well, making sure you’re covered from all sides.

const [user, setUser] = React.useState<IUser | null>(null);

// later...
setUser(newUser);

That way you’re ensuring you keep type checks, but allow for those scenarios where the initial value can come as null.

Generic Components

Much like the way you define generic functions and interfaces in TypeScript, you can define generic components, allowing you to re-use them for different data types. You can do this for props and states as well.

interface Props<T> {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
}

function List<T>(props: Props<T>) {
  const { items, renderItem } = props;
  const [state, setState] = React.useState<T[]>([]); 
  
  return (
    <div>
      {items.map(renderItem)}
    </div>
  );
}

You can then use the component either by taking advantage of type inference or directly specifying the data types, likes so:

ReactDOM.render(
  <List
    items={["a", "b"]} // type of 'string' inferred here
    renderItem={item => (
      <li key={item}>
        {item.trim()} //allowed, because we're working with 'strings' all around 
      </li>
    )}
  />,
  document.body
);
ReactDOM.render(
  <List<number>
    items={[1,2,3,4]} 
    renderItem={item => <li key={item}>{item.toPrecision(3)}</li>}
  />,
  document.body
);

For the latter, note that if your list contains strings instead of numbers, TypeScript will throw an error during the transpilation process.

Extending HTML Elements

Sometimes, your components function and behave like native HTML elements (on steroids). For example, a “borederd box” (which is simply a component that always renders a div with a default border) or a “big submit” (which again, is nothing but your good old submit button with a default size and maybe some custom behavior).

For these scenarios, it’s best to define your component type as a native HTML element or an extension of it.

export interface IBorderedBoxProps extends React.HTMLAttributes<HTMLDivElement> {
    title: string;
}

class BorderedBox extends React.Component<IBorderedBoxProps, void> {
    public render() {
        const {children, title, ...divAttributes} = this.props;

        return (
            //it is a DIV afterall, and we're trying to let the user use this component knowing that.
            <div {...divAttributes} style={{border: "1px solid red"}}>
                <h1>{title}</h1>
                {children}
            </div>
        );
    }
}

const myBorderedBox = <BorderedBox title="Hello" onClick={() => alert("Hello")}/>;

As you can see, I’ve extended HTML’s default props and added a new one: “title” for my specific needs.

Event Types

As you probably know, React provides its own set of events, which is why you can’t directly use the good old HTML Events. That being said, you do have access to all the useful UI events you need, so much so in fact, that they have the same names as well, so make sure you reference them directly like React.MouseEvent or just remember to import them from React like so:

import React, { Component, MouseEvent } from 'react';

The benefits of using TypeScript here, is that we can also use Generics (like in the previous example) to restrict the elements a particular event handler can be used on.

For example, the following code will not work:

function eventHandler(event: React.MouseEvent<HTMLAnchorElement>) {
    console.log("TEST!")
}

const ExtendedSelectableButton = ({text, type, action}: IButtonProps) => {
    
    let [selected, setSelected]  = useState(false)

    return (<button className={"extendedSelectableButton " + type + (selected? " selected" : "")} onClick={eventHandler}>{text}</button>)
}

And you’ll see an error message similar to the following:

You can, however, use unions to allow a single handler to be re-used by multiple components:

/** This will allow you to use this event handler both, on anchors and button elements */
function eventHandler(event: React.MouseEvent<HTMLAnchorElement | HTMLButtonElement>) {
    console.log("TEST!")
}

Integrated type definition

Finally, for the last tip, I wanted to mention React’s index.d.ts and the global.d.ts files. They’re both installed when you add React to your project (if you used npm, you’ll find them inside the npm_modules/@types/react folder.

These files contain type and interface definitions used by React, so if you need to understand the props of one particular type (or simply which types React makes available), you can open these files and review their content.

For example:

There you can see a small section of the index.d.ts file, showing the different signatures for the createElement function.

Conclusion

There is a lot more you can achieve by using TypeScript as part of your React toolchain.

Either way, I hope you got something out of this article, and feel free to leave any other tips or tricks you’ve picked up over the years of using TypeScript for your React projects!

Suggest