TypeScript for React Developers 2026: Build Type-Safe Applications
Master TypeScript in React development. From basic interfaces to advanced patterns, learn how to build robust, type-safe React applications.
Full Stack Developer | TypeScript Expert
Why TypeScript with React?
TypeScript adds static typing to JavaScript, catching errors during development rather than in production. When combined with React, it provides excellent IntelliSense, better refactoring support, and more maintainable code.
In this guide, we'll cover everything you need to know to use TypeScript effectively in your React applications.
Setting Up TypeScript with React
Modern React projects with Create React App or Next.js come with TypeScript support out of the box.
# Create React App with TypeScript npx create-react-app my-app --template typescript # Next.js with TypeScript npx create-next-app@latest my-app --typescript # Vite with React + TypeScript npm create vite@latest my-app -- --template react-ts
Basic TypeScript Types in React
Let's start with the fundamental types you'll use in React components.
import React from 'react';
// Basic component with TypeScript
const Counter = () => {
const [count, setCount] = React.useState<number>(0);
const [name, setName] = React.useState<string>('');
const handleClick = (): void => {
setCount(count + 1);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>Increment</button>
</div>
);
};
export default Counter;Typing Component Props with Interfaces
Interfaces define the shape of your component props, ensuring type safety.
import React from 'react';
// Define interface for props
interface UserCardProps {
user: {
id: number;
name: string;
email: string;
avatar?: string; // Optional property
};
onClick?: (id: number) => void; // Optional function
className?: string; // Optional string
}
// Component with typed props
const UserCard: React.FC<UserCardProps> = ({
user,
onClick,
className = ''
}) => {
const handleCardClick = (): void => {
if (onClick) {
onClick(user.id);
}
};
return (
<div className={className} onClick={handleCardClick}>
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
);
};
export default UserCard;Typing React Hooks
React hooks have specific typing patterns you should follow.
import React, { useState, useEffect, useRef } from 'react';
interface User {
id: number;
name: string;
email: string;
}
const UserProfile: React.FC<{ userId: number }> = ({ userId }) => {
// useState with typing
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
// useRef with typing
const inputRef = useRef<HTMLInputElement>(null);
// useEffect with proper cleanup
useEffect(() => {
const fetchUser = async (): Promise<void> => {
try {
setLoading(true);
const response = await fetch(`/api/users/${userId}`);
const userData: User = await response.json();
setUser(userData);
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
} finally {
setLoading(false);
}
};
fetchUser();
}, [userId]);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!user) return <div>User not found</div>;
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
<input ref={inputRef} type="text" placeholder="Type here..." />
</div>
);
};Using Generics in React
Generics make your components more reusable by allowing them to work with different types.
import React from 'react';
// Generic interface for list props
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
keyExtractor: (item: T) => string | number;
onItemClick?: (item: T) => void;
}
// Generic List component
const List = <T,>({
items,
renderItem,
keyExtractor,
onItemClick
}: ListProps<T>): React.ReactElement => {
return (
<ul>
{items.map(item => (
<li
key={keyExtractor(item)}
onClick={() => onItemClick?.(item)}
>
{renderItem(item)}
</li>
))}
</ul>
);
};
// Usage with different types
const UserList = () => {
const users = [
{ id: 1, name: 'John', email: 'john@example.com' },
{ id: 2, name: 'Jane', email: 'jane@example.com' }
];
return (
<List
items={users}
renderItem={(user) => <div>{user.name} - {user.email}</div>}
keyExtractor={(user) => user.id}
onItemClick={(user) => console.log('Clicked:', user.name)}
/>
);
};Custom Hooks with TypeScript
Custom hooks benefit greatly from TypeScript for better type safety.
import { useState, useEffect } from 'react';
// Generic API hook
interface UseApiResult<T> {
data: T | null;
loading: boolean;
error: string | null;
refetch: () => Promise<void>;
}
function useApi<T>(url: string): UseApiResult<T> {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
const fetchData = async (): Promise<void> => {
try {
setLoading(true);
const response = await fetch(url);
if (!response.ok) {
throw new Error('Network response was not ok');
}
const result: T = await response.json();
setData(result);
setError(null);
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchData();
}, [url]);
return { data, loading, error, refetch: fetchData };
}
// Usage
interface User {
id: number;
name: string;
email: string;
}
const UserProfile = ({ userId }: { userId: number }) => {
const { data: user, loading, error } = useApi<User>(`/api/users/${userId}`);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!user) return <div>User not found</div>;
return <div>{user.name}</div>;
};Typing Event Handlers
Properly typing event handlers prevents runtime errors and provides better IntelliSense.
import React, { FormEvent, ChangeEvent, MouseEvent } from 'react';
const FormExample: React.FC = () => {
const [email, setEmail] = React.useState<string>('');
const [password, setPassword] = React.useState<string>('');
// Form submit handler
const handleSubmit = (e: FormEvent<HTMLFormElement>): void => {
e.preventDefault();
console.log('Form submitted:', { email, password });
};
// Input change handler
const handleEmailChange = (e: ChangeEvent<HTMLInputElement>): void => {
setEmail(e.target.value);
};
// Button click handler
const handleButtonClick = (e: MouseEvent<HTMLButtonElement>): void => {
console.log('Button clicked');
};
// Generic change handler
const handleInputChange = <T extends HTMLInputElement | HTMLTextAreaElement>(
e: ChangeEvent<T>
): void => {
const { name, value } = e.target;
if (name === 'email') {
setEmail(value);
} else if (name === 'password') {
setPassword(value);
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
name="email"
value={email}
onChange={handleInputChange}
placeholder="Email"
/>
<input
type="password"
name="password"
value={password}
onChange={handleInputChange}
placeholder="Password"
/>
<button type="submit">Submit</button>
<button type="button" onClick={handleButtonClick}>
Click me
</button>
</form>
);
};Advanced TypeScript Patterns
Let's explore some advanced patterns for more complex scenarios.
import React from 'react';
// Discriminated unions for component variants
type ButtonVariant = 'primary' | 'secondary' | 'danger';
interface BaseButtonProps {
children: React.ReactNode;
onClick?: () => void;
disabled?: boolean;
}
interface PrimaryButtonProps extends BaseButtonProps {
variant: 'primary';
color?: 'blue' | 'green';
}
interface SecondaryButtonProps extends BaseButtonProps {
variant: 'secondary';
outline?: boolean;
}
interface DangerButtonProps extends BaseButtonProps {
variant: 'danger';
confirmText?: string;
}
type ButtonProps = PrimaryButtonProps | SecondaryButtonProps | DangerButtonProps;
const Button: React.FC<ButtonProps> = (props) => {
const { children, onClick, disabled = false } = props;
const getClassName = (): string => {
const baseClass = 'button';
const disabledClass = disabled ? 'disabled' : '';
switch (props.variant) {
case 'primary':
return `${baseClass} primary ${props.color || 'blue'} ${disabledClass}`;
case 'secondary':
return `${baseClass} secondary ${props.outline ? 'outline' : ''} ${disabledClass}`;
case 'danger':
return `${baseClass} danger ${disabledClass}`;
default:
return baseClass;
}
};
return (
<button className={getClassName()} onClick={onClick} disabled={disabled}>
{children}
</button>
);
};
// Usage
const App = () => {
return (
<div>
<Button variant="primary" color="blue" onClick={() => console.log('Primary')}>
Primary Button
</Button>
<Button variant="secondary" outline onClick={() => console.log('Secondary')}>
Secondary Button
</Button>
<Button variant="danger" confirmText="Are you sure?" onClick={() => console.log('Danger')}>
Danger Button
</Button>
</div>
);
};Utility Types in React
TypeScript provides utility types that are particularly useful in React development.
import React from 'react';
// Partial - Make all properties optional
interface UserForm {
name: string;
email: string;
age: number;
}
const updateUser = (updates: Partial<UserForm>): void => {
// updates.name, updates.email, and updates.age are all optional
console.log(updates);
};
// Pick - Select specific properties
interface User {
id: number;
name: string;
email: string;
password: string;
}
type PublicUser = Pick<User, 'id' | 'name' | 'email'>;
const UserCard: React.FC<{ user: PublicUser }> = ({ user }) => {
return (
<div>
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
);
};
// Omit - Remove specific properties
type CreateUserRequest = Omit<User, 'id'>;
const CreateUserForm: React.FC<{
onSubmit: (user: CreateUserRequest) => void;
}> = ({ onSubmit }) => {
const handleSubmit = (formData: CreateUserRequest) => {
onSubmit(formData);
};
// Form implementation...
return <div>Form</div>;
};
// Record - Create object types
type ThemeColors = Record<'primary' | 'secondary' | 'success', string>;
const ThemeProvider: React.FC<{ colors: ThemeColors }> = ({ colors }) => {
return (
<div style={{ color: colors.primary }}>
Themed content
</div>
);
};TypeScript Best Practices for React
✅ Do's
- • Use interfaces for component props
- • Type all event handlers properly
- • Use generics for reusable components
- • Prefer explicit return types for functions
- • Use utility types when appropriate
- • Enable strict mode in tsconfig.json
❌ Don'ts
- • Don't use 'any' unless absolutely necessary
- • Don't skip typing event handlers
- • Don't ignore TypeScript errors
- • Don't over-complicate type definitions
- • Don't forget to type custom hooks
- • Don't disable TypeScript rules without good reason
TypeScript Configuration
Here's a recommended tsconfig.json for React projects:
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "es6"],
"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",
"baseUrl": "src",
"paths": {
"@/*": ["*"],
"@/components/*": ["components/*"],
"@/hooks/*": ["hooks/*"],
"@/types/*": ["types/*"]
}
},
"include": ["src"],
"exclude": ["node_modules"]
}Conclusion
TypeScript brings significant benefits to React development, including better error catching, improved developer experience, and more maintainable code. While it might require some initial learning, the long-term benefits are well worth it.
Start with basic typing and gradually incorporate more advanced patterns as you become comfortable. The key is consistency and gradually improving your type definitions as your applications grow.
Happy coding with TypeScript and React!
Ready to Build Type-Safe Apps?
Explore more TypeScript and React tutorials to level up your development skills!