How to use React with TypeScript
TypeScript adds static typing to your React projects, catching bugs before they reach production and providing better IDE support. Your components become more reliable with properly typed props and state, making it easier to maintain and refactor code. This guide walks you through creating React applications with TypeScript, from project setup to typing components and working with state management.
Starting a React Project with TypeScript
The first step in using static typing in your React applications is setting up a project with TypeScript. If you're starting from scratch, Create React App provides a straightforward TypeScript template. Run this command to create a new project::
npx create-react-app my-app --template typescript
This creates a new React project in a folder named my-app
, with TypeScript setup and ready to go.
Using TypeScript Types in a React Application
Defining and using types for props and state is vital for keeping type safety in your React components. TypeScript interfaces let you define the structure of objects, including properties and their types. Here's how you can define an interface for a User
:
interface User {
id: number;
name: string;
email: string;
}
You can then use this type to check your components and make sure they get the right props and handle user data correctly:
function UserProfile({ user }: { user: User }) {
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}
Typing React Components with TypeScript
Typing React components with TypeScript means defining types for props, state, and context. Use React.FC
for functional components and React.Component
for class components. Here's a typed functional component:
type GreetingProps = {
name: string;
isLoggedIn?: boolean;
}
function Greeting({ name, isLoggedIn = false }: GreetingProps) {
return (
<h1>
{isLoggedIn ? `Welcome back, ${name}!` : `Hello, ${name}`}
</h1>
);
}
And here's a typed class component:
interface CounterState {
count: number;
}
interface CounterProps {
startFrom: number;
}
class Counter extends React.Component<CounterProps, CounterState> {
state = {
count: this.props.startFrom
};
increment = () => {
this.setState(state => ({
count: state.count + 1
}));
};
render() {
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={this.increment}>Add</button>
</div>
);
}
}
When working with event handlers, TypeScript helps prevent common mistakes:
function Form() {
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
// Form handling logic
};
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
// Input handling logic
};
return (
<form onSubmit={handleSubmit}>
<input type="text" onChange={handleChange} />
<button type="submit">Submit</button>
</form>
);
}
Creating Reusable Components with Generics
Generics let you create components that work with different data types. Here's a generic List
component:
interface ListProps<T> {
items: T[];
renderItem: (item: T) => JSX.Element;
}
function List<T>(props: ListProps<T>) {
return (
<ul>
{props.items.map(props.renderItem)}
</ul>
);
}
You can use this component with various data types:
const numbers = [1, 2, 3];
const strings = ['a', 'b', 'c'];
return (
<div>
<List items={numbers} renderItem={(item) => <li>{item}</li>} />
<List items={strings} renderItem={(item) => <li>{item}</li>} />
</div>
);
You can also use utility types like Partial<T>
to create variations of your props:
interface ButtonProps {
text: string;
onClick: () => void;
disabled?: boolean;
className?: string;
}
// Make all props optional
const LoadingButton = (props: `Partial<ButtonProps>`) => {
return <button {...props}>Loading...</button>;
};
Using TypeScript with React State Management Libraries
TypeScript can help manage global state in your application when used with built-in React state management or with libraries like Redux and MobX. For local state management with hooks:
function useCounter(initialValue: number = 0) {
const [count, setCount] = useState(initialValue);
const increment = () => setCount(prev => prev + 1);
const decrement = () => setCount(prev => prev - 1);
return {
count,
increment,
decrement
} as const;
}
Using React's Context API with TypeScript:
interface ThemeContext {
isDark: boolean;
toggleTheme: () => void;
}
const ThemeContext = React.createContext<ThemeContext | undefined>(undefined);
function useTheme() {
const context = React.useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within ThemeProvider');
}
return context;
}
Here's how you can use TypeScript with Redux:
import { createStore } from 'redux';
interface CounterState {
count: number;
}
const counterReducer = (state: CounterState = { count: 0 }, action: any) => {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 };
case 'DECREMENT':
return { count: state.count - 1 };
default:
return state;
}
};
const store = createStore(counterReducer);
And here's how to use TypeScript with MobX:
import { makeAutoObservable } from 'mobx';
class Counter {
count = 0;
constructor() {
makeAutoObservable(this);
}
increment() {
this.count++;
}
decrement() {
this.count--;
}
}
Handling Props and State in React Components with TypeScript
TypeScript helps manage props and state in React components by catching type errors before runtime. Here's a todo list component demonstrating typed record state management and map transformations:
interface TodoItem {
id: number;
text: string;
completed: boolean;
}
interface TodoListProps {
initialTodos?: TodoItem[];
onTodoToggle: (id: number) => void;
}
function TodoList({ initialTodos = [], onTodoToggle }: TodoListProps) {
const [todos, setTodos] = useState<TodoItem[]>(initialTodos);
const [newTodoText, setNewTodoText] = useState('');
const addTodo = () => {
if (newTodoText.trim()) {
setTodos(prev => [...prev, {
id: Date.now(),
text: newTodoText,
completed: false
}]);
setNewTodoText('');
}
};
return (
<div>
<input
type="text"
value={newTodoText}
onChange={e => setNewTodoText(e.target.value)}
placeholder="Add new todo"
/>
<button onClick={addTodo}>Add</button>
<ul>
{todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => onTodoToggle(todo.id)}
/>
<span>{todo.text}</span>
</li>
))}
</ul>
</div>
);
}
Using type assertions when needed:
function ImageUpload() {
const fileInputRef = useRef<HTMLInputElement>(null);
const handleUpload = () => {
const file = fileInputRef.current?.files?.[0];
if (file) {
// Handle file upload
}
};
return (
<div>
<input type="file" ref={fileInputRef} />
<button onClick={handleUpload}>Upload</button>
</div>
);
}
For Convex applications, Convex's React client library provides type-safe data fetching and state management.
Setting Up TypeScript for a React Project
To add TypeScript to an existing React project:
Install TypeScript and React type definitions:
npm install typescript @types/react @types/react-dom
Create a tsconfig.json file
Run this command:
npx tsc --init
Update the tsconfig.json
with React-specific settings
{
"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,
"outDir": "build",
"jsx": "react-jsx"
},
"include": ["src/**/*"]
}
Rename your .js
and .jsx
files to .ts
and .tsx
. TypeScript will immediately highlight type errors, which you can fix gradually. Finally, Update your imports to use ES modules syntax if necessary:
// Old
const React = require('react');
// New
import React from 'react';
Handling Complex Forms in React with TypeScript
Managing complex forms with TypeScript involves defining types for form data and ensuring correct usage. Here's a typed Form
component:
interface FormData {
name: string;
email: string;
}
interface FormProps {
formData: FormData;
onSubmit: (formData: FormData) => void;
}
const Form: React.FC<FormProps> = ({ formData, onSubmit }) => {
const [name, setName] = React.useState(formData.name);
const [email, setEmail] = React.useState(formData.email);
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
onSubmit({ name, email });
};
return (
<form onSubmit={handleSubmit}>
<label>
Name:
<input type="text" value={name} onChange={(event) => setName(event.target.value)} />
</label>
<label>
Email:
<input type="email" value={email} onChange={(event) => setEmail(event.target.value)} />
</label>
<button type="submit">Submit</button>
</form>
);
};
Building Type-Safe React Applications
React and TypeScript work together to help you catch errors early and write more maintainable code. Remember these key points:
-
Define clear interfaces for your props and state
-
Use TypeScript's built-in types for React's event handlers
-
Take advantage of generics for reusable components
-
Let type inference work for you when possible
For more complex applications:
-
Create custom hooks with proper type definitions
-
Type-safe forms with validation
-
State management patterns
-
Component testing strategies
Need a type-safe backend for your React and TypeScript project? Get started with Convex to see how it integrates with your frontend.