Skip to main content

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.