Declaring React Components with TypeScript

React is a popular JavaScript library for building user interfaces, and TypeScript is a typed superset of JavaScript that helps catch errors early in the development process. Combining React with TypeScript can significantly enhance the development experience by providing type safety, better code readability, and improved maintainability. In this blog post, we will explore the fundamental concepts, usage methods, common practices, and best practices of declaring React components with TypeScript.

Table of Contents

  1. Fundamental Concepts
  2. Usage Methods
  3. Common Practices
  4. Best Practices
  5. Conclusion
  6. References

Fundamental Concepts

TypeScript adds static typing to JavaScript, allowing developers to define the types of variables, function parameters, and return values. When working with React components in TypeScript, we can define the types of props and state to ensure that the component receives the correct data and behaves as expected.

Props

Props are used to pass data from a parent component to a child component. In TypeScript, we can define the types of props using interfaces or types.

State

State is used to manage the internal data of a component. Similar to props, we can define the types of state in TypeScript.

Usage Methods

Function Components

Function components are the recommended way to write React components in modern React applications. In TypeScript, we can define the types of props for a function component using an interface or a type.

import React from 'react';

// Define the props interface
interface HelloProps {
  name: string;
  age?: number;
}

// Define the function component
const Hello: React.FC<HelloProps> = ({ name, age }) => {
  return (
    <div>
      <p>Hello, {name}!</p>
      {age && <p>You are {age} years old.</p>}
    </div>
  );
};

export default Hello;

In the above example, we define an interface HelloProps to specify the types of props for the Hello component. The name prop is required, while the age prop is optional.

Class Components

Class components are another way to write React components. In TypeScript, we can define the types of props and state for a class component using generics.

import React, { Component } from 'react';

// Define the props interface
interface CounterProps {
  initialCount: number;
}

// Define the state interface
interface CounterState {
  count: number;
}

// Define the class component
class Counter extends Component<CounterProps, CounterState> {
  constructor(props: CounterProps) {
    super(props);
    this.state = {
      count: props.initialCount,
    };
  }

  increment = () => {
    this.setState((prevState) => ({
      count: prevState.count + 1,
    }));
  };

  render() {
    return (
      <div>
        <p>Count: {this.state.count}</p>
        <button onClick={this.increment}>Increment</button>
      </div>
    );
  }
}

export default Counter;

In the above example, we define an interface CounterProps to specify the types of props and an interface CounterState to specify the types of state for the Counter component.

Common Practices

Prop Types

Defining prop types is essential for ensuring that the component receives the correct data. We can use interfaces or types to define prop types.

import React from 'react';

// Define the props interface
interface UserProps {
  name: string;
  email: string;
  isAdmin: boolean;
}

// Define the function component
const User: React.FC<UserProps> = ({ name, email, isAdmin }) => {
  return (
    <div>
      <p>Name: {name}</p>
      <p>Email: {email}</p>
      <p>Admin: {isAdmin ? 'Yes' : 'No'}</p>
    </div>
  );
};

export default User;

Default Props

We can define default values for props using the defaultProps property.

import React from 'react';

// Define the props interface
interface MessageProps {
  text: string;
  color?: string;
}

// Define the function component
const Message: React.FC<MessageProps> = ({ text, color }) => {
  return (
    <p style={{ color }}>
      {text}
    </p>
  );
};

// Define default props
Message.defaultProps = {
  color: 'black',
};

export default Message;

State Types

Defining state types is important for ensuring that the component’s internal data is of the correct type. We can use interfaces or types to define state types.

import React, { Component } from 'react';

// Define the state interface
interface TodoState {
  todos: string[];
  newTodo: string;
}

// Define the class component
class TodoList extends Component<{}, TodoState> {
  constructor(props: {}) {
    super(props);
    this.state = {
      todos: [],
      newTodo: '',
    };
  }

  handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    this.setState({
      newTodo: e.target.value,
    });
  };

  handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    this.setState((prevState) => ({
      todos: [...prevState.todos, prevState.newTodo],
      newTodo: '',
    }));
  };

  render() {
    return (
      <div>
        <form onSubmit={this.handleSubmit}>
          <input
            type="text"
            value={this.state.newTodo}
            onChange={this.handleChange}
          />
          <button type="submit">Add Todo</button>
        </form>
        <ul>
          {this.state.todos.map((todo, index) => (
            <li key={index}>{todo}</li>
          ))}
        </ul>
      </div>
    );
  }
}

export default TodoList;

Best Practices

Use React.FC with Caution

The React.FC type has some limitations, such as not supporting defaultProps and children in the same way as a regular function component. It is recommended to use a regular function component type definition instead.

import React from 'react';

// Define the props interface
interface CardProps {
  title: string;
  children?: React.ReactNode;
}

// Define the function component
const Card = ({ title, children }: CardProps) => {
  return (
    <div>
      <h2>{title}</h2>
      {children}
    </div>
  );
};

export default Card;

Separate Type Definitions

It is a good practice to separate type definitions into their own files, especially for larger projects. This makes the code more modular and easier to maintain.

// types.ts
export interface ProductProps {
  name: string;
  price: number;
}

// Product.tsx
import React from 'react';
import { ProductProps } from './types';

const Product: React.FC<ProductProps> = ({ name, price }) => {
  return (
    <div>
      <p>{name}</p>
      <p>${price}</p>
    </div>
  );
};

export default Product;

Leverage Type Inference

TypeScript can often infer types automatically, so we don’t always need to explicitly define types. This can make the code more concise.

import React from 'react';

const Button = ({ text }: { text: string }) => {
  return <button>{text}</button>;
};

export default Button;

Conclusion

Declaring React components with TypeScript can bring many benefits, such as type safety, better code readability, and improved maintainability. By understanding the fundamental concepts, usage methods, common practices, and best practices, developers can write more robust and reliable React applications. Remember to define prop types, state types, and use TypeScript’s features effectively to catch errors early in the development process.

References