DEV Community

Daniel Einars
Daniel Einars

Posted on

React TypeScript Basics

This article was originally published on my personal website here. There's some other react/typescript related content there as well if you want to have a look.

UPDATE

React.FC isn't really used anymore and I've removed/replaced all examples using this.

1. Overview

After a quick introduction to Typescript I'll highlight some brief examples & explanations of Typescript usage with React Children, React Hooks and HTMLElements (with and without React).
Finally, I'll sum up the differences between Interface and Type (and when to use which) as well as showing how to use untyped libraries in Typescript projects.

2. Typed Props

type Props can be declared as a type or as an interface.

What should I use? According to this article

If you write object-oriented code — use interfaces, if you write functional code — use type aliases.

The main difference is

interfaces are more “extendable” due to the support of declaration merging, and types are more “composable” due to support of union types.

Basically, you can extend existing interfaces which might become confusing in a large code base.

Example:

interface IUser {
  firstName: string
  lastName: string
}

interface IUser {
  age: number
}

const user: IUser = {
  firstName: 'Jon',
  lastName: 'Doe',
  age: 25,
}
Enter fullscreen mode Exit fullscreen mode

Now to the course. This is how you declare and use a type in a Functional TS React Component

import React from 'react';

// type declaration:
type Props = {
  title: string,
  isActive: boolean
}

export const Header= ({title, isActive}:Props) => {
  return (
    <>
      <h1>{title}</h1>
      {isActive && <h3> Active</h3>}
    </>
  );
};
Enter fullscreen mode Exit fullscreen mode

3. Default and/or Optional Props

Props can be marked as optional using the ?. If this is done, the component requires the prop to contain a default value.

import React from 'react';


type Props = {
  title: string,
  isActive?: boolean // optional
}

// isActive is now optional and therefor needs a default argument in case this isn't provided.
export const Header = ({title, isActive= true}:Props) => {

[...]

}

Enter fullscreen mode Exit fullscreen mode

4. Types

This video covers some common types. It does not go into specifics about how you would create these when you'd actually use the <Header [...]/> component.

// User Type
type User = {
  name: string;
}
// Common types.
type Props = {
  title: string, //strings
  isActive?: boolean //booleans
  thing: number // numbers
  thing2: string[] //arrays of strings || booleans || numbers
  status: 'loading' | 'loaded' // Union Type. Accepts only one of these two strings
  thing3: object // this is an empty object. can also be declared using {}
//  thing4: User | {}  the above is typically declared like this, if at all.
  thing4: {
    name: string;
  }
  func: () => void // Define functions. Quite common
  user: User
}
Enter fullscreen mode Exit fullscreen mode

5. Function Props

Way in which a function can be typed.

import React from 'react';


type Props = {
  // onClick: Function // function (lower-case "f") is not recognized. Topic of chapter 7.
  // method returns a string
  // onClick(): string // function which returns a string.

  // method which does not anything.
  // onClick(value: string): void

  // most common way to type a function.
  onClick: (text: string) => void;
}


export const Button = ({onClick}: Props) => {
  return <button onClick={() => onClick("hi there")}>Click Me</button>;
};
Enter fullscreen mode Exit fullscreen mode

Accompanying App.tsx. Notice that I don't pass in the value to be logged in App.tsx but in Button.tsx

import React from 'react';
import './App.css';
import { Header } from './components/Header';
import { Button } from './components/Button';

const App = () => {

  return (
    <>
      <Header
        title="{'Hello'}"
      />
      <Button
        onClick={(text) => {
          console.log(text);
        }}
      />
    </>
  );
};

export default App;

Enter fullscreen mode Exit fullscreen mode

6. React Events in TypeScript

React has its own set of Events, such as React.MouseEvent. This will accept events for all clicks (bad)

import React from "react"

type Props = {
  onClick: (e: React.MouseEvent) => void;
}
Enter fullscreen mode Exit fullscreen mode

Now it is specified to only accept events from a button click (good)

import React from "react"

type Props = {
  onClick: (e: React.MouseEvent<HTMLButtonElement>) => void
}

Enter fullscreen mode Exit fullscreen mode

7. Typing Children Props

The example below is receiving a string element as a child.

Because we are passing a string into the button component, this will work. However, down the line it will become difficult because you might want to pass a myriad of various
components into other components.

import React from 'react';

type Props = {
  onClick: (e: React.MouseEvent<HTMLButtonElement>) => void
  children: string;
}

// Placing the Prop declaration into the arrow handles. Use this only when children are in the mix. 
// Otherwise keep on using the previous syntax.
export const Button = ({onClick, children: Props}) => {
  return <button onClick={onClick}>{children}</button>;
};
Enter fullscreen mode Exit fullscreen mode

In order to solve this, declare the react component with React.FC<Props> (see Update). Placing the Prop declaration into the arrow handles. Use this only when children are in the mix. Otherwise keep on using the previous syntax. This way it merges the Props which are expected from React.FC with the props we declared.

import React from 'react';

type Props = {
  onClick: (e: React.MouseEvent<HTMLButtonElement>) => void
}

export const Button = ({onClick, children}:Props) => {
  return <button onClick={onClick}>{children}</button>;
};
Enter fullscreen mode Exit fullscreen mode

8. Typing React.useState

Because the default state is set to '', typescript will automatically recognized that the useState instance accepts texts values. TypeScript is doing it's job implicity.

import React from 'react';

export const Input = () => {
  const [name, setName] = React.useState('');
  return (
    <input value="{name}" onChange={e => setName(e.target.value)}/>
  );
};
Enter fullscreen mode Exit fullscreen mode

Using the union type brackets (<>) we can define a state to be either of type string OR null. This will still throw an error because "value" can not be null.

import React, { useState } from 'react';

export const Input = () => {
  const [name, setName] = useState<string | null>("");

  return (
    <input value="{name}" onChange={e => setName(e.target.value)}/>
  );
};
Enter fullscreen mode Exit fullscreen mode

Recommendation is to set the initial type to what the state expects. If that isn't enough, union types can be used.

9. Using React.useRef and typing dom elements

Type refs by declaring what type of HTML Element they will be attached to. Adding the "!" to the end of "null" declares it as a read-only value (very typical for refs)

import React, { useRef, useState } from 'react';

export const Input = () => {
  const [name, setName] = useState('');
  const ref = useRef<HTMLInputElement>(null!);

  console.log(ref?.current?.value);

  return (
    <input ref="{ref}" value="{name}" onChange={e => setName(e.target.value)}/>
  );
};
Enter fullscreen mode Exit fullscreen mode

10. Using React.useReducer

Typing useReducer is no different than previous standard typing. You define types for Action & State and assign these to the initialState and the reducer function. Optionally add a payload to the action which can be typed as anything previously covered.

import React, { useReducer } from 'react';

const initialState: State = {rValue: true};

type State = {
  rValue: boolean
}

type Action = {
  type: string
  // Optionally define a payload of any type (object, boolean, etc.)
  // payload: string
}

const reducer = (state: State, action: Action) => {
  switch (action.type) {
    case 'one': {
      return {
        rValue: true
      };
    }
    case 'two': {
      return {
        rValue: false
      };
    }
    // Required in order to tell TS what state should be accepted in the default case.
    default: {
      return state;
    }
  }
};

export const ReducerButtons = () => {

  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      <button onClick={() => dispatch({type: 'one'})}>Action One</button>
      <button onClick={() => dispatch({type: 'two'})}>Action Two</button>
      {state?.rValue ? <h1>visible</h1> : 'invisible'}
    </>
  );
};
Enter fullscreen mode Exit fullscreen mode

In order to stop the reducers from accepting bad actions, you can type these as well using UntionTypes.

type Action = {
  type: 'one' | 'two' // using unionTypes to declare a specific set of allowed actions.
  // Optionally define a payload of any type (object, boolean, etc.)
  // payload: string
}


// Syntax for declaring multiple actions. This can also be used to type which payloads will be accepted.
type ActionExtended =
  | { type: 'one', payload: boolean }
  | { type: 'two' }
  | { type: 'three' }
  | { type: 'four' }
  | { type: 'five' };


Enter fullscreen mode Exit fullscreen mode

11. React.useEffect and Custom Hooks

This custom hook es designed to execute some code depending on weather a ref is clicked or not (close modal type).

It takes a ref to the HTML Element and the handler function. Because the handler function adds DOM listeners (as opposed to React listeners)
it has to be declared as standard Mouse/Touch Events (both need to be declared because we could be on a mobile device).

Additionally, because we are mutating the reference, we need to declare the ref as a React.MutableRefObject<HTMLElement>.


import React, { useEffect } from 'react';


const useClickOutside = (
  ref: React.MutableRefObject<HTMLElement>,
  handler: (event: MouseEvent | TouchEvent) => void) => {
  useEffect(() => {
    const listener = (event: MouseEvent | TouchEvent) => {
      if (!ref.current || ref.current.contains(event.target as Node)) {
        return;
      }
      handler(event);
    };

    document.addEventListener('mousedown', listener);
    document.addEventListener('touchstart', listener );
    return () => {
      document.removeEventListener('mousedown', listener);
      document.removeEventListener('touchstart', listener);

    };
  }, [handler, ref]);
};

export { useClickOutside };
Enter fullscreen mode Exit fullscreen mode

12. Generics

By example of the useClickOutside hook. Before we declared its ref type as ref: React.MutableRefObject<HTMLDivElement>. This did work, but did not allow us to use it on elements other than <div/>. Because HTMLDivElement extends HTMLElement, we can declare the ref in the hook as ref: React.MutableRefObject<HTMLElement>. When it's actually used, the ref can contain any HTMLElement and TS will not throw any errors.

13. React.useContext

TS implicitly types the declared context without having to type it. Below is an example implementation.

import { createContext } from 'react';

export const initialValues = {
  rValue: true
};

export const GlobalContext = createContext(initialValues);
Enter fullscreen mode Exit fullscreen mode

We can use this example in ReducerButtons.tsx like this.

import { useContext } from "react";
import { GlobalContext } from './GlobalState';

const {rValue} = useContext(GlobalContext);
Enter fullscreen mode Exit fullscreen mode

Attempts to destructor properties, which don't exist, will throw TS errors.

import { useContext } from "react";
import { GlobalContext } from './GlobalState';

const {does-not-exist} = useContext(GlobalContext);
Enter fullscreen mode Exit fullscreen mode

This works just great for most cases. We can also type the context should we want to.

import { createContext } from 'react';

type InitialValues = {
  rValue: boolean
}

export const initialValues: InitialValues = {
  rValue: true
};

export const GlobalContext = createContext(initialValues);
Enter fullscreen mode Exit fullscreen mode

Next we update the GlobalState component by creating a GlobalProvider. This saves us from
having to import initialValues where ever we use it and can instead just wrap the GlobalProvider component.
Additionally we refactored GlobalState.tsx to include the useReducer function from the ReducerButtons.tsx
component.

The refactored GlobalState.tsx component looks like this now

import React, { createContext, useReducer } from 'react';


export const initialValues = {
  rValue: true,
  // This creates an implied context without having to explicitly type it.
  // It allows us to assign the dispatch functions (turnOn & turnOff) in the `value` prop
  // passed in the `<GlobalContext.Provider ...` component
  turnOn: () => {},
  turnOff: () => {}
};

export const GlobalContext = createContext(initialValues);

type State = {
  rValue: boolean
}

type Action = {
  type: 'on' | 'off'
}
const reducer = (state: State, action: Action) => {
  switch (action.type) {
    case 'on': {
      return {
        rValue: true
      };
    }
    case 'off': {
      return {
        rValue: false
      };
    }
    default: {
      return state;
    }
  }
};


export const GlobalProvider = ({children}) => {
  const [state, dispatch] = useReducer(reducer, initialValues);

  return (
    <GlobalContext.Provider value="{{"
      rValue: state.rValue,
      // Functions have been added to the initialValues object. These are implicitly typed. 
      turnOn: () => dispatch({type: 'on'}),
      turnOff: () => dispatch({type: 'off'})
    }}>
      {children}
    </GlobalContext.Provider>
  );
};
Enter fullscreen mode Exit fullscreen mode

The ReducerButtons.tsx component has been updated to work with GlobalState.tsx.

import React, { useContext, useRef } from 'react';
import { useClickOutside } from './useClickOutside';
import { GlobalContext } from './GlobalState';


export const ReducerButtons = () => {
  const ref = useRef<HTMLDivElement>(null!);
  const context = useContext(GlobalContext);
  const {rValue, turnOn, turnOff} = context; // Functions extracted from the GlobalContext 


  useClickOutside(ref, () => {
    console.log('Clicked Outside'); // handler passed into useClickOutside
  });
  return (
    <div ref="{ref}">
      // These functions are implicitly typed by the initialValues object in GlobalState.tsx
      <button onClick={turnOn}>Action One</button> 
      <button onClick={turnOff}>Action Two</button>
      {rValue ? <h1>visible</h1> : <h1>invisible</h1>}
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

14. Class Based Components

Classes and FC are quite similar. The following example illustrates a simple example.

type and state definitions are placed inside generic braces (< and >).

import React, { Component } from 'react';

type State = {
  shouldRender: boolean
}


type Props = {
  title: string;
}

class BigC extends Component<Props, State> {
  render() {
    return (
      <div>
        <h1>I'm in a class component</h1>
      </div>
    );
  }
}

export default BigC;
Enter fullscreen mode Exit fullscreen mode

15. Interfaces Types

The gist from several articles is

When you come from OOP, use interface, if you're into functional programming, go for type

Pros for Types:

  • More constrained
  • Better for intersecting
  • Better in error messages
  • Can be used in unions

Pros for Interfaces:

  • Better when authoring a library
  • Extendable
  • Can be augmented

Here are some helpful tips and tricks to help you decide. In the end, what ever you choose, stick to it!

16. Using untyped libraries

Some libraries you'd like touse in your projects don't provide their own types. In this scenario you have two options:

  1. Declare the types yourself in a *.d.ts file
  2. Install the types (usually @types/LIBRARYMANE) from the DefinitelyTyped repository.

An example *.d.ts file can look as simple as this:

declare module 'styled-components' // This can allow you to use third-party un-typed libs.
Enter fullscreen mode Exit fullscreen mode

Top comments (0)