React
TypeScript
Web Development
Frontend
JavaScript
Programming
Tutorial

React and TypeScript: A Complete Guide

Learn React with TypeScript from scratch: type components and props, manage state with useState, write generic components, and set up a project with Vite.

12 min read
Chamikara Nayanajith

React is a popular JavaScript library for building user interfaces, and TypeScript is a strongly typed superset of JavaScript that adds static typing to the language. Combining React with TypeScript can enhance your development experience by providing better tooling, catching errors early, and improving code maintainability. This comprehensive guide will walk you through setting up a React project with TypeScript, covering everything from installation to advanced patterns and best practices.

Why Use TypeScript with React?

Before diving into the implementation, let's explore why TypeScript is an excellent choice for React development:

  • Type Safety: Catch errors at compile time rather than runtime, reducing bugs in production.
  • Better IDE Support: Enhanced autocomplete, refactoring tools, and inline documentation improve developer productivity.
  • Improved Code Quality: TypeScript encourages better code structure and makes codebases more maintainable as they grow.
  • Team Collaboration: Types serve as documentation, making it easier for team members to understand and work with your code.

Prerequisites

Before diving into React with TypeScript, ensure you have the following prerequisites installed:

  • Node.js: Make sure Node.js (version 14 or higher) is installed on your machine. You can download it from the official Node.js website.
  • npm or yarn: These are package managers for JavaScript. npm comes bundled with Node.js, or you can use yarn, which is another popular package manager.
  • Code Editor: A code editor like Visual Studio Code (VS Code) is recommended for its excellent support for both React and TypeScript, including IntelliSense, debugging, and extensions.

Setting Up a New Project

To create a new React project with TypeScript, you can use Create React App (CRA), which provides a zero-configuration setup. Alternatively, you can use Vite for a faster development experience.

Using Create React App

Create React App is the most popular way to bootstrap a new React application with TypeScript support:

  1. Open your terminal or command prompt.
  2. Run the following command to create a new React project with TypeScript support:
bash
npx create-react-app my-app --template typescript
# or
yarn create react-app my-app --template typescript
  1. Navigate into your project directory:
bash
cd my-app
  1. Start the development server:
bash
npm start
# or
yarn start

Your new React and TypeScript project should now be running on http://localhost:3000.

Using Vite (Alternative)

Vite is a modern build tool that provides faster development server startup and hot module replacement:

bash
npm create vite@latest my-app -- --template react-ts
# or
yarn create vite my-app --template react-ts

Then navigate to the project directory and install dependencies:

bash
cd my-app
npm install
# or
yarn install

Basic Concepts

Now that your project is set up, let's explore the fundamental concepts of using TypeScript with React.

Components with TypeScript

In React, components are the building blocks of your application. When using TypeScript, you can define props with types to ensure that the component receives the correct data. This provides compile-time type checking and better IDE support.

Functional Component Example

Here's a basic example of a typed functional component:

tsx
import React from 'react';

interface GreetingProps {
  name: string;
  age?: number; // Optional prop
}

const Greeting: React.FC<GreetingProps> = ({ name, age }) => {
  return (
    <div>
      <h1>Hello, {name}!</h1>
      {age && <p>You are {age} years old.</p>}
    </div>
  );
};

export default Greeting;

In this example, we define an interface GreetingProps that specifies the type of the props. The React.FC (Function Component) type is a generic type provided by React that includes children props by default. The age prop is marked as optional using the ? operator.

Alternative: Direct Function Declaration

You can also type components without using React.FC:

tsx
interface GreetingProps {
  name: string;
  age?: number;
}

function Greeting({ name, age }: GreetingProps) {
  return (
    <div>
      <h1>Hello, {name}!</h1>
      {age && <p>You are {age} years old.</p>}
    </div>
  );
}

export default Greeting;

State Management with TypeScript

Managing state in a React component with TypeScript involves defining the type of the state object. TypeScript can usually infer the type from the initial value, but explicit typing can be helpful for complex state. Once state outgrows a single component, it is worth comparing React state management libraries such as Zustand and Jotai.

Using the useState Hook

Here's how to use the useState hook with TypeScript:

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

interface User {
  name: string;
  email: string;
}

const UserProfile: React.FC = () => {
  // TypeScript infers the type from the initial value
  const [count, setCount] = useState<number>(0);
  
  // For complex objects, explicitly define the type
  const [user, setUser] = useState<User | null>(null);
  
  // For arrays, specify the array element type
  const [items, setItems] = useState<string[]>([]);

  const handleIncrement = () => {
    setCount(count + 1);
  };

  const handleSetUser = () => {
    setUser({
      name: 'John Doe',
      email: 'john@example.com',
    });
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleIncrement}>Increment</button>
      
      {user && (
        <div>
          <p>Name: {user.name}</p>
          <p>Email: {user.email}</p>
        </div>
      )}
      <button onClick={handleSetUser}>Set User</button>
    </div>
  );
};

export default UserProfile;

In this example, we explicitly specify the type of the state using the generic type parameter. For count, we use <number>, and for user, we use <User | null> to indicate it can be either a User object or null.

Events and Event Handlers

Handling events in React with TypeScript involves defining the correct types for event handlers. React provides built-in types for common events.

Example: Handling a Button Click

Here's how to properly type event handlers:

tsx
import React from 'react';

const Button: React.FC = () => {
  const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
    event.preventDefault();
    // Example: console.log for demonstration purposes
    console.log('Button clicked!', event);
  };

  return <button onClick={handleClick}>Click me</button>;
};

export default Button;

Example: Handling Form Input

For form inputs, you'll typically use React.ChangeEvent:

tsx
import React, { useState, ChangeEvent, FormEvent } from 'react';

const ContactForm: React.FC = () => {
  const [email, setEmail] = useState<string>('');
  const [message, setMessage] = useState<string>('');

  const handleEmailChange = (event: ChangeEvent<HTMLInputElement>) => {
    setEmail(event.target.value);
  };

  const handleMessageChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
    setMessage(event.target.value);
  };

  const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    // Example: console.log for demonstration purposes
    console.log('Form submitted:', { email, message });
    // Handle form submission
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}
        onChange={handleEmailChange}
        placeholder="Your email"
      />
      <textarea
        value={message}
        onChange={handleMessageChange}
        placeholder="Your message"
      />
      <button type="submit">Submit</button>
    </form>
  );
};

export default ContactForm;

Using useEffect Hook

The useEffect hook is commonly used for side effects in React. With TypeScript, you don't need to add explicit types for the effect function, but you should be aware of cleanup functions:

tsx
import React, { useState, useEffect } from 'react';

interface Data {
  id: number;
  title: string;
}

const DataFetcher: React.FC = () => {
  const [data, setData] = useState<Data | null>(null);
  const [loading, setLoading] = useState<boolean>(true);

  useEffect(() => {
    let isMounted = true;

    const fetchData = async () => {
      try {
        const response = await fetch('https://api.example.com/data');
        const result: Data = await response.json();
        
        if (isMounted) {
          setData(result);
          setLoading(false);
        }
      } catch (error) {
        if (isMounted) {
          // Example: console.error for demonstration purposes
          // In production, you would handle errors appropriately (e.g., show user-friendly message)
          console.error('Error fetching data:', error);
          setLoading(false);
        }
      }
    };

    fetchData();

    // Cleanup function
    return () => {
      isMounted = false;
    };
  }, []); // Empty dependency array means this runs once on mount

  if (loading) return <div>Loading...</div>;
  if (!data) return <div>No data available</div>;

  return <div>{data.title}</div>;
};

export default DataFetcher;

Best Practices

To make the most of TypeScript with React, consider these best practices:

  • Use Interfaces for Props: Define clear interfaces for component props to ensure type safety and improve code readability.
  • Leverage Type Inference: TypeScript can often infer types automatically. Don't over-annotate when the type is obvious.
  • Use Type Guards: Implement type guards to narrow types and ensure type safety when working with union types.
  • Avoid Using `any`: While any can be convenient, it defeats the purpose of TypeScript. Use unknown or proper types instead.
  • Organize Types: Keep type definitions in separate files or at the top of component files for better organization.

Common Patterns

Here are some common patterns you'll encounter when building React applications with TypeScript:

Children Props

When working with components that accept children, you can use React.ReactNode:

tsx
import React, { ReactNode } from 'react';

interface CardProps {
  title: string;
  children: ReactNode;
}

const Card: React.FC<CardProps> = ({ title, children }) => {
  return (
    <div className="card">
      <h2>{title}</h2>
      <div>{children}</div>
    </div>
  );
};

export default Card;

Refs with TypeScript

When using refs, you need to specify the element type:

tsx
import React, { useRef, useEffect } from 'react';

const InputFocus: React.FC = () => {
  const inputRef = useRef<HTMLInputElement>(null);

  useEffect(() => {
    // Focus the input when component mounts
    inputRef.current?.focus();
  }, []);

  return <input ref={inputRef} type="text" placeholder="Focus me!" />;
};

export default InputFocus;

Generic Components

You can create generic components for reusable, type-safe code:

tsx
import React from 'react';

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

function List<T>({ items, renderItem }: ListProps<T>) {
  return (
    <ul>
      {items.map((item, index) => (
        <li key={index}>{renderItem(item)}</li>
      ))}
    </ul>
  );
}

// Usage
interface User {
  id: number;
  name: string;
}

const UserList: React.FC = () => {
  const users: User[] = [
    { id: 1, name: 'Alice' },
    { id: 2, name: 'Bob' },
  ];

  return (
    <List
      items={users}
      renderItem={(user) => <span>{user.name}</span>}
    />
  );
};

export default UserList;

Conclusion

Combining React with TypeScript can significantly improve your development workflow by providing better type safety, enhanced tooling, and improved code maintainability. This guide covered the basics of setting up a React project with TypeScript, defining component props, managing state, handling events, and implementing common patterns.

As you continue to explore React with TypeScript, you'll discover more advanced features and patterns that can help you build robust, scalable, and maintainable applications. The type system will become your ally in catching bugs early and making your codebase more predictable.

Start with the fundamentals covered in this article, and gradually incorporate more advanced TypeScript features as your projects grow in complexity. Happy coding!

Further Reading

By incorporating these resources and continuing to explore React and TypeScript together, you'll be able to build more robust, type-safe, and maintainable React applications. The combination of React's component-based architecture and TypeScript's type system creates a powerful development experience that scales with your projects.

Related Articles