Building Typesafe components in React using Typescript

Photo by Emre Turkan on Unsplash

Building Typesafe components in React using Typescript

Building typesafe components in React is important for maintaining a healthy codebase and ensuring that your application is free of runtime errors. In this article, we will explore how to use the TypeScript type system to create type-safe React components.

What are types?

In a type system, a type is a set of values that a variable or expression can take on. For example, the type string represents a set of possible string values such as 'hello' or 'world'. In TypeScript, we can specify the type of a variable by using a type annotation. Here is an example:

let message: string = 'hello world';

In this example, the variable message has the type string. This means that message can only be assigned string values. If we try to assign a non-string value to message, the TypeScript compiler will throw an error.

Type safety in React

In a React application, components are the building blocks that make up the user interface. By making our components type-safe, we can ensure that they are used correctly and that the data they receive is valid.

To create a type-safe React component, we first need to define the types for the props (short for properties) that the component expects to receive. A prop is an input to a component, similar to an argument to a function. For example, a Button component might accept a label prop to specify the text to display on the button.

Here is an example of a type-safe Button component:

type ButtonProps = {
  label: string;
  onClick: () => void;
};

const Button: React.FC<ButtonProps> = ({ label, onClick }) => (
  <button onClick={onClick}>{label}</button>
);

In this example, the ButtonProps type defines the shape of the props that the Button component expects to receive. The Button component is defined as a function component (React.FC) that takes a generic type parameter ButtonProps. This tells the TypeScript compiler that the Button component expects to receive props of type ButtonProps.

Now, when we use the Button component, the TypeScript compiler will check that we are passing the correct props to the component. For example, the following code will cause a compile-error because it is missing the required label prop:

<Button onClick={() => {}} />

Type inference

In the example above, we explicitly defined the type of the Button component's props using the ButtonProps type. However, we can also use the TypeScript compiler to infer the types of the props automatically.

Here is the same Button component using type inference:

const Button: React.FC<{ label: string; onClick: () => void }> = ({ label, onClick }) => (
  <button onClick={onClick}>{label}</button>
);

In this example, we removed the ButtonProps type and instead specified the types of the props directly in the generic type parameter of the React.FC type. The TypeScript compiler will infer the types of the props from the function's parameter types.

TypeScript and JSX

TypeScript has full support for JSX, the syntax used to define React components. In a JSX element, we can specify the type of the props using the props attribute. Here is an example:

const Greeting: React.FC<{ name: string }> = ({ name }) => (
  <div>Hello, {name}</div>
);

In this example, the Greeting component expects to receive a single prop called name of type string.

Defining state types

In addition to props, a React component can also have state, which is data that is managed internally by the component.

Class based Component

To define the types for a component's state, we can use the React.Component class and the state attribute. Here is an example:

type CounterState = {
  count: number;
};

class Counter extends React.Component<{}, CounterState> {
  state: CounterState = {
    count: 0,
  };

  render() {
    return <div>{this.state.count}</div>;
  }
}

In this example, the Counter component has a state of type CounterState, which is defined as an object with a single field count of type number.

Function based Component

Similarly, we can write types for state in function based component as follows :

import { useState } from "react";

type CounterState = {
  count: number;
};

export default function Counter() { 
    const [counterState, setCounterState] = useState<CounterState>({
        count: 0,
    })

    return <div>{counterState.count}</div>
}

Type-safe events

React components often handle events, such as clicks or form submissions. To ensure that the event handlers are type-safe, we can use the React.SyntheticEvent type.

Here is an example of a type-safe event handler:

type FormProps = {
  onSubmit: (event: React.SyntheticEvent) => void;
};

const Form: React.FC<FormProps> = ({ onSubmit }) => (
  <form onSubmit={onSubmit}>
    <button type="submit">Submit</button>
  </form>
);

In this example, the Form component expects to receive an onSubmit prop of type (event: React.SyntheticEvent) => void. This means that the onSubmit prop is a function that takes a single argument of type React.SyntheticEvent and returns void.

Conclusion

In this article, we learned how to use the TypeScript type system to create type-safe React components. By using types in our React code, we can catch errors at compile-time and ensure that our components are used correctly. This can help us build more reliable and maintainable applications.