TypeScript in React: A Practical Guide to Better Code
If you've been working with React and want to level up your development experience, TypeScript is your next best friend. In this guide, we'll explore how to effectively use TypeScript in React projects, from basic setup to advanced patterns.
Why TypeScript in React?
Before diving in, let's understand why TypeScript makes sense for React projects:
β¨ Better Developer Experience
Autocomplete and IntelliSense
Catch errors before runtime
Better refactoring capabilities
π‘οΈ Type Safety
Prevent common prop errors
Ensure correct data structures
Validate function parameters and returns
π Self-Documenting Code
Props requirements are clear
Function signatures are explicit
Component API is well-defined
Getting Started
Setting Up a New Project
# Create a new React project with TypeScript
npx create-react-app my-app --template typescript
# Or add TypeScript to existing project
npm install --save typescript @types/node @types/react @types/react-dom @types/jest
Basic TypeScript Concepts in React
1. Component Props
// Basic props interface
interface ButtonProps {
text: string;
onClick: () => void;
disabled?: boolean; // Optional prop
}
// Function component with typed props
const Button: React.FC<ButtonProps> = ({ text, onClick, disabled = false }) => {
return (
<button
onClick={onClick}
disabled={disabled}
>
{text}
</button>
);
};
// Usage
<Button
text="Click me"
onClick={() => console.log('clicked')}
/>
2. State with TypeScript
interface User {
id: number;
name: string;
email: string;
}
function UserProfile() {
// Type inference works here
const [loading, setLoading] = useState(false);
// Explicit type for complex objects
const [user, setUser] = useState<User | null>(null);
return (
<div>
{loading ? (
<div>Loading...</div>
) : user ? (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
) : null}
</div>
);
}
3. Event Handling
interface FormState {
email: string;
password: string;
}
function LoginForm() {
const [form, setForm] = useState<FormState>({
email: '',
password: ''
});
// Typed event handler
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setForm(prev => ({
...prev,
[name]: value
}));
};
// Form submission handler
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
// Type-safe access to form values
console.log(form.email, form.password);
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
name="email"
value={form.email}
onChange={handleChange}
/>
<input
type="password"
name="password"
value={form.password}
onChange={handleChange}
/>
<button type="submit">Login</button>
</form>
);
}
Advanced Patterns
1. Generic Components
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 users: User[] = [
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' }
];
<List
items={users}
renderItem={(user) => user.name}
/>
2. Type Guards
interface AdminUser {
type: 'admin';
name: string;
privileges: string[];
}
interface RegularUser {
type: 'regular';
name: string;
}
type User = AdminUser | RegularUser;
// Type guard function
function isAdmin(user: User): user is AdminUser {
return user.type === 'admin';
}
function UserPrivileges({ user }: { user: User }) {
if (isAdmin(user)) {
// TypeScript knows user is AdminUser here
return <div>Privileges: {user.privileges.join(', ')}</div>;
}
// TypeScript knows user is RegularUser here
return <div>Regular user: {user.name}</div>;
}
3. Context with TypeScript
interface ThemeContextType {
theme: 'light' | 'dark';
toggleTheme: () => void;
}
const ThemeContext = React.createContext<ThemeContextType | undefined>(undefined);
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
// Custom hook for using theme
export function useTheme() {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}
Project Organization
Here's a recommended way to organize TypeScript files in a React project:
src/
βββ types/
β βββ user.ts
β βββ api.ts
β βββ common.ts
βββ components/
β βββ Button/
β β βββ Button.tsx
β β βββ Button.types.ts
β β βββ index.ts
β βββ Card/
β βββ Card.tsx
β βββ Card.types.ts
β βββ index.ts
βββ hooks/
β βββ useUser.ts
β βββ useApi.ts
βββ utils/
βββ typeGuards.ts
Type Definitions Example
// types/user.ts
export interface User {
id: number;
name: string;
email: string;
role: 'admin' | 'user';
}
// types/api.ts
export interface ApiResponse<T> {
data: T;
status: number;
message: string;
}
// components/Button/Button.types.ts
export interface ButtonProps {
variant: 'primary' | 'secondary';
size: 'small' | 'medium' | 'large';
onClick: () => void;
children: React.ReactNode;
}
Best Practices
- Define Props Interface Separately
// Bad
const Component = ({ name, age }: { name: string; age: number }) => {...}
// Good
interface ComponentProps {
name: string;
age: number;
}
const Component: React.FC<ComponentProps> = ({ name, age }) => {...}
- Use Type Inference When Possible
// Let TypeScript infer simple state types
const [isOpen, setIsOpen] = useState(false);
// Explicitly type complex states
const [user, setUser] = useState<User | null>(null);
- Export Types for Reuse
// types/common.ts
export type Status = 'idle' | 'loading' | 'success' | 'error';
export type Theme = 'light' | 'dark';
// Use throughout your app
import { Status, Theme } from '../types/common';
- Use Enum for Constants
enum HttpStatus {
OK = 200,
CREATED = 201,
BAD_REQUEST = 400,
UNAUTHORIZED = 401
}
function handleResponse(status: HttpStatus) {
if (status === HttpStatus.OK) {
// Handle success
}
}
TypeScript Configuration
A recommended tsconfig.json
for React projects:
{
"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",
"baseUrl": "src"
},
"include": ["src"]
}
Conclusion
TypeScript adds a powerful layer of type safety and developer experience to React projects. By following these patterns and best practices, you can:
Write more maintainable code
Catch errors early in development
Improve team collaboration
Create self-documenting components
Enable better tooling support
Remember, TypeScript is not about making your code more complicatedβit's about making it more predictable and maintainable. Start with basic type annotations and gradually move to more advanced patterns as your comfort level increases.
Happy coding! π
If you found this guide helpful, don't forget to like and share! Have questions about TypeScript in React? Drop them in the comments below!
Subscribe to my newsletter
Read articles from Pawan Gangwani directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Pawan Gangwani
Pawan Gangwani
Iβm Pawan Gangwani, a passionate Full Stack Developer with over 12 years of experience in web development. Currently serving as a Lead Software Engineer at Lowes India, I specialize in modern web applications, particularly in React and performance optimization. Iβm dedicated to best practices in coding, testing, and Agile methodologies. Outside of work, I enjoy table tennis, exploring new cuisines, and spending quality time with my family.