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.
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:
- Open your terminal or command prompt.
- Run the following command to create a new React project with TypeScript support:
npx create-react-app my-app --template typescript
# or
yarn create react-app my-app --template typescript- Navigate into your project directory:
cd my-app- Start the development server:
npm start
# or
yarn startYour 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:
npm create vite@latest my-app -- --template react-ts
# or
yarn create vite my-app --template react-tsThen navigate to the project directory and install dependencies:
cd my-app
npm install
# or
yarn installBasic 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:
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:
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:
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:
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:
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:
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
anycan be convenient, it defeats the purpose of TypeScript. Useunknownor 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:
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:
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:
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.


