TypeScript React
Introduction
React is one of the most popular JavaScript libraries for building user interfaces, and TypeScript is a powerful type system that enhances JavaScript development. When combined, they create a robust ecosystem for building scalable, maintainable web applications with fewer runtime errors and improved developer experience.
TypeScript React (often referred to as TSX) brings static typing to React applications, enabling developers to catch errors during development rather than at runtime, provide better tooling support, and improve code documentation through explicit type definitions.
In this guide, we'll learn how to use TypeScript with React, covering the fundamental concepts, best practices, and practical examples.
Setting Up a TypeScript React Project
Using Create React App
The easiest way to start a new TypeScript React project is by using Create React App with the TypeScript template:
npx create-react-app my-app --template typescript
# or with yarn
yarn create react-app my-app --template typescript
Manual Setup
If you're adding TypeScript to an existing React project, you'll need to:
- Install TypeScript and React type definitions:
npm install --save typescript @types/node @types/react @types/react-dom
# or with yarn
yarn add typescript @types/node @types/react @types/react-dom
- Create a
tsconfig.json
file in your project root:
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": ["src"]
}
- Rename your
.js
files to.tsx
for React components or.ts
for plain TypeScript files.
Basic TypeScript React Concepts
JSX vs TSX
In regular React, we use JSX (JavaScript XML) to write components. With TypeScript, we use TSX (TypeScript XML). The main difference is that TSX allows you to add type annotations to your components.
A basic .tsx
file looks like this:
import React from 'react';
// A simple TypeScript React component
const Greeting: React.FC = () => {
return <h1>Hello, TypeScript React!</h1>;
};
export default Greeting;
Typing Component Props
One of the biggest benefits of TypeScript is the ability to type-check props passed to components:
import React from 'react';
// Define the props interface
interface UserCardProps {
name: string;
email: string;
age: number;
isActive?: boolean; // Optional prop with '?'
}
// Use the interface to type the component props
const UserCard: React.FC<UserCardProps> = ({ name, email, age, isActive = true }) => {
return (
<div className={`user-card ${isActive ? 'active' : 'inactive'}`}>
<h2>{name}</h2>
<p>Email: {email}</p>
<p>Age: {age}</p>
<p>Status: {isActive ? 'Active' : 'Inactive'}</p>
</div>
);
};
export default UserCard;
With this approach, TypeScript will ensure that all required props are provided when using the component:
// This will compile successfully
<UserCard name="John Doe" email="[email protected]" age={28} />
// This will raise a TypeScript error because 'age' is missing
<UserCard name="Jane Smith" email="[email protected]" />
Alternative Ways to Type Components
There are multiple ways to define a component with TypeScript:
// 1. Using React.FC (Function Component)
const Component1: React.FC<Props> = (props) => {
return <div>{props.children}</div>;
};
// 2. Using a function declaration with Props type
function Component2(props: Props): JSX.Element {
return <div>{props.title}</div>;
}
// 3. Using arrow function with explicit return type
const Component3 = (props: Props): JSX.Element => {
return <div>{props.content}</div>;
};
Each approach has its own nuances, but they all achieve the same goal of adding type safety to your components.
Typing Hooks
useState with TypeScript
With TypeScript, you can explicitly type your state variables:
import React, { useState } from 'react';
const Counter: React.FC = () => {
// Basic primitive type
const [count, setCount] = useState<number>(0);
// For complex types
const [user, setUser] = useState<{ name: string; age: number } | null>(null);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
{user ? (
<p>User: {user.name}, Age: {user.age}</p>
) : (
<button onClick={() => setUser({ name: "John", age: 30 })}>Set User</button>
)}
</div>
);
};
export default Counter;
In many cases, TypeScript can infer the type from the initial value, but explicit typing is helpful for complex types or when the initial value is null
.
useEffect with TypeScript
useEffect
doesn't typically need explicit typing as it's inferred from the functions you provide:
import React, { useState, useEffect } from 'react';
const UserProfile: React.FC = () => {
const [userId, setUserId] = useState<number>(1);
const [user, setUser] = useState<{ name: string; email: string } | null>(null);
const [loading, setLoading] = useState<boolean>(true);
useEffect(() => {
const fetchUser = async () => {
try {
setLoading(true);
// TypeScript ensures we're handling the response correctly
const response = await fetch(`https://api.example.com/users/${userId}`);
const data = await response.json();
setUser({ name: data.name, email: data.email });
} catch (error) {
console.error('Failed to fetch user:', error);
setUser(null);
} finally {
setLoading(false);
}
};
fetchUser();
}, [userId]); // TypeScript checks that dependencies are correctly listed
return (
<div>
{loading ? (
<p>Loading...</p>
) : user ? (
<div>
<h2>{user.name}</h2>
<p>Email: {user.email}</p>
</div>
) : (
<p>User not found</p>
)}
<button onClick={() => setUserId(userId + 1)}>Next User</button>
</div>
);
};
export default UserProfile;
useContext with TypeScript
When using useContext
, TypeScript helps ensure you're using the context values correctly:
import React, { createContext, useContext, useState } from 'react';
// Define the shape of your context
interface ThemeContextType {
isDarkMode: boolean;
toggleTheme: () => void;
}
// Create the context with a default value
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
// Provider component
export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [isDarkMode, setIsDarkMode] = useState<boolean>(false);
const toggleTheme = () => {
setIsDarkMode(prev => !prev);
};
// The value passed to the provider is typed
const value: ThemeContextType = {
isDarkMode,
toggleTheme
};
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
};
// Custom hook to use the theme context
export const useTheme = (): ThemeContextType => {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
};
// Example usage in a component
const ThemedButton: React.FC = () => {
const { isDarkMode, toggleTheme } = useTheme();
return (
<button
onClick={toggleTheme}
style={{
backgroundColor: isDarkMode ? '#333' : '#f0f0f0',
color: isDarkMode ? '#fff' : '#000',
padding: '8px 16px',
border: 'none',
borderRadius: '4px'
}}
>
Toggle Theme (Current: {isDarkMode ? 'Dark' : 'Light'})
</button>
);
};
export default ThemedButton;
Handling Events
TypeScript provides type definitions for DOM events, making it easier to handle them correctly:
import React, { useState } from 'react';
const Form: React.FC = () => {
const [inputValue, setInputValue] = useState<string>('');
// Event is typed as React.ChangeEvent<HTMLInputElement>
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(event.target.value);
};
// Event is typed as React.FormEvent<HTMLFormElement>
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
console.log('Form submitted with:', inputValue);
setInputValue('');
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={inputValue}
onChange={handleChange}
placeholder="Type something..."
/>
<button type="submit">Submit</button>
</form>
);
};
export default Form;
Common event types include:
React.ChangeEvent<HTMLInputElement>
- For input change eventsReact.FormEvent<HTMLFormElement>
- For form submission eventsReact.MouseEvent<HTMLButtonElement>
- For click events on buttonsReact.KeyboardEvent<HTMLInputElement>
- For keyboard events
Advanced Patterns
Generic Components
You can create reusable components with generics:
import React from 'react';
// Generic list component that can work with any item type
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
}
// The generic type T is used throughout the component
function List<T>({ items, renderItem }: ListProps<T>): JSX.Element {
return (
<ul>
{items.map((item, index) => (
<li key={index}>{renderItem(item)}</li>
))}
</ul>
);
}
// Usage
const App: React.FC = () => {
const numbers = [1, 2, 3, 4, 5];
const users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
];
return (
<div>
<h2>Number List</h2>
<List
items={numbers}
renderItem={(num) => <span>Number {num}</span>}
/>
<h2>User List</h2>
<List
items={users}
renderItem={(user) => <span>{user.id}: {user.name}</span>}
/>
</div>
);
};
export default App;
Higher-Order Components (HOCs)
TypeScript can help type your HOCs correctly:
import React, { ComponentType } from 'react';
// Define the props that will be injected by the HOC
interface WithLoadingProps {
loading: boolean;
}
// Define the HOC that adds a loading state
function withLoading<T extends object>(Component: ComponentType<T & WithLoadingProps>) {
// Return a component with the original component's props plus our loading prop
return function WithLoadingComponent({ loading = false, ...props }: WithLoadingProps & Partial<T>) {
// Cast props to T because we can't fully type-check it here
return loading ? <div>Loading...</div> : <Component loading={loading} {...(props as T)} />;
};
}
// Component that will use the HOC
interface UserProfileProps extends WithLoadingProps {
username: string;
}
const UserProfile: React.FC<UserProfileProps> = ({ loading, username }) => {
return (
<div>
<h2>User Profile</h2>
{loading ? <p>Loading...</p> : <p>Username: {username}</p>}
</div>
);
};
// Apply the HOC
const UserProfileWithLoading = withLoading(UserProfile);
// Usage
const App: React.FC = () => {
return (
<div>
<UserProfileWithLoading loading={false} username="johndoe" />
</div>
);
};
export default App;
Real-World Example: Todo Application
Let's build a simple todo application using TypeScript and React:
import React, { useState } from 'react';
// Define our Todo item type
interface Todo {
id: number;
text: string;
completed: boolean;
}
const TodoApp: React.FC = () => {
// State with proper typing
const [todos, setTodos] = useState<Todo[]>([]);
const [input, setInput] = useState<string>('');
// Add a new todo item
const handleAddTodo = (e: React.FormEvent) => {
e.preventDefault();
if (input.trim() === '') return;
const newTodo: Todo = {
id: Date.now(),
text: input,
completed: false
};
setTodos([...todos, newTodo]);
setInput('');
};
// Toggle todo completion status
const toggleTodo = (id: number) => {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
};
// Delete a todo
const deleteTodo = (id: number) => {
setTodos(todos.filter(todo => todo.id !== id));
};
return (
<div className="todo-app">
<h1>Todo App</h1>
<form onSubmit={handleAddTodo}>
<input
type="text"
value={input}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setInput(e.target.value)}
placeholder="Add a new task"
/>
<button type="submit">Add</button>
</form>
<ul>
{todos.map(todo => (
<li key={todo.id} style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
<span>{todo.text}</span>
<button onClick={() => deleteTodo(todo.id)}>Delete</button>
</li>
))}
</ul>
<div>
<p>Total todos: {todos.length}</p>
<p>Completed: {todos.filter(todo => todo.completed).length}</p>
</div>
</div>
);
};
export default TodoApp;
Best Practices for TypeScript in React
-
Define explicit interfaces/types for props:
tsxinterface ButtonProps {
onClick: () => void;
label: string;
disabled?: boolean;
} -
Create shared type definitions: Create a
types.ts
file to house shared type definitions that are used across multiple components. -
Use type inference when possible: Let TypeScript infer types when it's clear, but add annotations when it helps with readability or correctness.
-
Enable strict mode in tsconfig.json: This catches more potential issues and enforces better typing.
-
Use discriminated unions for complex state:
tsxtype State =
| { status: 'loading' }
| { status: 'error', error: Error }
| { status: 'success', data: User[] }; -
Type your API responses: Create interfaces to represent your API responses, making sure your fetch handlers know what data to expect.
-
Use readonly for immutable props:
tsxinterface ListProps {
readonly items: readonly string[];
}
Common TypeScript-React Pitfalls
-
Not typing children properly:
tsx// Correct way
interface CardProps {
children: React.ReactNode;
} -
Forgetting to type event handlers:
tsx// Incorrect
const handleClick = (e) => { /* ... */ };
// Correct
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => { /* ... */ }; -
Using
any
type too liberally: Avoid usingany
as it defeats the purpose of TypeScript. Useunknown
if the type is truly not known. -
Not handling null/undefined values: Use optional chaining (
?.
) and nullish coalescing (??
) operators to safely handle potentially undefined values. -
Incorrect generic typing:
tsx// Incorrect
const [user, setUser] = useState<User>(); // No initial value
// Correct
const [user, setUser] = useState<User | null>(null);
Summary
TypeScript brings significant benefits to React development by providing static type checking, better intellisense, and improved code documentation. In this guide we've covered:
- Setting up a TypeScript React project
- Typing components and their props
- Working with hooks in TypeScript
- Handling events with proper type definitions
- Advanced patterns like generic components and HOCs
- A real-world todo application example
- Best practices and common pitfalls
By combining TypeScript with React, you create a development environment that catches errors early, improves developer experience, and results in more robust applications.
Additional Resources
To continue learning TypeScript with React:
- Official TypeScript React documentation: https://www.typescriptlang.org/docs/handbook/react.html
- React TypeScript Cheatsheet: https://react-typescript-cheatsheet.netlify.app/
- TypeScript Deep Dive book: https://basarat.gitbook.io/typescript/
Exercises
- Convert an existing JavaScript React application to TypeScript.
- Create a shopping cart application with TypeScript, implementing features like product listing, cart management, and checkout.
- Build a form with complex validation using TypeScript to ensure type safety for form inputs and validation rules.
- Implement a data fetching component that uses TypeScript generics to handle different API response types.
- Create a theme provider using React Context API with TypeScript to provide strong typing for theme values and actions.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)