TypeScript for React: Complete Guide with Practical Examples

TypeScript adds static type checking to JavaScript, catching errors before they reach production. Combined with React, it provides powerful tools for building robust, maintainable applications.

TypeScript with React enables: IDE autocomplete, compile-time error detection, better documentation through types, improved refactoring safety, and self-documenting code.

Type Safety Benefits:
TypeScript prevents runtime errors by catching type mismatches during development. It’s particularly valuable in React for typing props, state, and component APIs where mistakes are common.

Setting Up TypeScript in React

Create a New React + TypeScript Project

// Using Vite (recommended)
npm create vite@latest my-app — –template react-ts
cd my-app
npm install// Or using Create React App
npx create-react-app my-app –template typescript

Project Structure

src/
├── components/
│ ├── Button.tsx
│ ├── Header.tsx
│ └── types.ts
├── hooks/
│ └── useUserData.ts
├── types/
│ └── api.ts
├── App.tsx
├── main.tsx
└── vite-env.d.ts

TypeScript Configuration

{
“compilerOptions”: {
“target”: “ES2020”,
“useDefineForClassFields”: true,
“lib”: [“ES2020”, “DOM”, “DOM.Iterable”],
“module”: “ESNext”,
“skipLibCheck”: true,
“strict”: true,
“jsx”: “react-jsx”
}
}

Basic Types and Interfaces

Primitive Types

// Basic types
let name: string = ‘John’;
let age: number = 25;
let isActive: boolean = true;
let nothing: undefined = undefined;
let empty: null = null;// Union types
let id: string | number;
id = 123;
id = ‘abc’;// Literal types
type Status = ‘pending’ | ‘completed’ | ‘failed’;
let status: Status = ‘pending’;

Interfaces

Define the shape of objects:

interface User {
id: number;
name: string;
email: string;
age?: number; // optional
readonly createdAt: Date; // readonly
}const user: User = {
id: 1,
name: ‘John’,
email: ‘john@example.com’,
createdAt: new Date()
};

Type vs Interface

Feature Type Interface
Define object shape
Union types
Extend/Merge Intersection (&) extends
Primitives
✅ React Best Practice: Use interfaces for component props and types for everything else.

Typing React Props

Basic Props Typing

interface ButtonProps {
label: string;
onClick: () => void;
disabled?: boolean;
variant?: ‘primary’ | ‘secondary’;
}export function Button({
label,
onClick,
disabled = false,
variant = ‘primary’
}: ButtonProps) {
return (
<button
onClick={onClick}
disabled={disabled}
className={`btn btn-${variant}`}
>
{label}
</button>
);
}

Props with Event Handlers

interface InputProps {
value: string;
onChange: (value: string) => void;
onBlur?: (e: React.FocusEvent<HTMLInputElement>) => void;
placeholder?: string;
}export function Input(props: InputProps) {
return (
<input
type=”text”
value={props.value}
onChange={(e) => props.onChange(e.target.value)}
onBlur={props.onBlur}
placeholder={props.placeholder}
/>
);
}

React.ReactNode for Children

interface CardProps {
title: string;
children: React.ReactNode;
}export function Card({ title, children }: CardProps) {
return (
<div className=”card”>
<h2>{title}</h2>
<div className=”card-content”>{children}</div>
</div>
);
}// Usage
<Card title=”Welcome”>
<p>This is the content</p>
</Card>

Using FC (Function Component) Type

interface HeaderProps {
title: string;
}export const Header: React.FC<HeaderProps> = ({ title }) => {
return <header><h1>{title}</h1></header>;
};

Typing State and Hooks

useState with Types

import { useState } from ‘react’;function Counter() {
// TypeScript infers type as number
const [count, setCount] = useState(0);// Explicit type annotation
const [name, setName] = useState<string>();

// Complex type
const [user, setUser] = useState<User | null>(null);

return (
<div>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}

useEffect with Types

import { useEffect } from ‘react’;interface UserData {
id: number;
name: string;
}function UserProfile({ userId }: { userId: number }) {
const [user, setUser] = useState<UserData | null>(null);
const [loading, setLoading] = useState<boolean>(true);

useEffect(() => {
const fetchUser = async () => {
try {
const response = await fetch(`/api/users/${userId}`);
const data: UserData = await response.json();
setUser(data);
} finally {
setLoading(false);
}
};

fetchUser();
}, [userId]);

if (loading) return <p>Loading…</p>;
return <div>{user?.name}</div>;
}

useContext with Types

import { createContext, useContext } from ‘react’;interface ThemeContextType {
theme: ‘light’ | ‘dark’;
toggleTheme: () => void;
}const ThemeContext = createContext<ThemeContextType | undefined>(undefined);

export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error(‘useTheme must be used within ThemeProvider’);
}
return context;
}

Handling Events with Types

Event Handler Types

interface FormProps {}export function Form({}: FormProps) {
// Click event
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
console.log(e.currentTarget.tagName);
};// Change event
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
};

// Form submission
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
};

// Focus event
const handleFocus = (e: React.FocusEvent<HTMLInputElement>) => {
console.log(‘Focused on:’, e.target.id);
};

return (
<form onSubmit={handleSubmit}>
<input onChange={handleChange} onFocus={handleFocus} />
<button onClick={handleClick}>Submit</button>
</form>
);
}

Keyboard Events

export function SearchInput() {
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === ‘Enter’) {
console.log(‘Search for:’, e.currentTarget.value);
}
};return (
<input
type=”text”
onKeyDown={handleKeyDown}
placeholder=”Search…”
/>
);
}

Generic Components

Generic List Component

interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
keyExtractor: (item: T) => string | number;
}export function List<T,>({
items,
renderItem,
keyExtractor
}: ListProps<T>) {
return (
<ul>
{items.map((item) => (
<li key={keyExtractor(item)}>
{renderItem(item)}
</li>
))}
</ul>
);
}// Usage
interface Product {
id: number;
name: string;
}

<List<Product>
items={products}
renderItem={(p) => p.name}
keyExtractor={(p) => p.id}
/>

Generic HOC (Higher-Order Component)

function withLoading<P,>(
Component: React.ComponentType<P>
) {
return function LoadingComponent(
props: P & { loading: boolean }
) {
const { loading, …rest } = props;if (loading) {
return <p>Loading…</p>;
}return <Component {…(rest as P)} />;
};
}

// Usage
const UserListWithLoading = withLoading(UserList);

Advanced Patterns

Discriminated Unions

Type-safe state management patterns:

type AsyncState<T> =
| { status: ‘idle’ }
| { status: ‘loading’ }
| { status: ‘success’; data: T }
| { status: ‘error’; error: Error };function useAsync<T,>(
fn: () => Promise<T>
): AsyncState<T> {
const [state, setState] = useState<AsyncState<T>>({
status: ‘idle’
});return state;
}

// Usage with type narrowing
const state = useAsync(() => fetchUser());

switch (state.status) {
case ‘loading’:
return <p>Loading…</p>;
case ‘success’:
return <p>{state.data.name}</p>;
case ‘error’:
return <p>Error: {state.error.message}</p>;
default:
return null;
}

Utility Types

interface User {
id: number;
name: string;
email: string;
}// Pick – select specific properties
type UserPreview = Pick<User, ‘name’ | ’email’>;// Omit – exclude specific properties
type UserWithoutId = Omit<User, ‘id’>;

// Partial – make all properties optional
type UserUpdate = Partial<User>;

// Record – create object with specific keys
type UserRoles = Record<‘admin’ | ‘user’ | ‘guest’, boolean>;

Conditional Types

type IsString<T> = T extends string ? true : false;type A = IsString<‘hello’>; // true
type B = IsString<42>; // falsetype Flatten<T> = T extends Array<infer U> ? U : T;

type Str = Flatten<string[]>; // string
type Num = Flatten<number>; // number

Best Practices

1. Avoid `any` at All Costs

// ❌ Don’t: Using any
function process(data: any) {
return data.map((x: any) => x.value);
}// ✅ Do: Use proper types
function process(data: { value: number }[]) {
return data.map((x) => x.value);
}

2. Use Strict Mode

// tsconfig.json
{
“compilerOptions”: {
“strict”: true,
“noImplicitAny”: true,
“strictNullChecks”: true,
“strictFunctionTypes”: true
}
}

3. Organize Types in Separate Files

// types/index.ts
export interface User {
id: number;
name: string;
}export interface Post {
id: number;
title: string;
userId: number;
}// components/UserProfile.tsx
import { User, Post } from ‘../types’;

4. Leverage Type Inference

// Let TypeScript infer types when possible
const users = [
{ id: 1, name: ‘John’ },
{ id: 2, name: ‘Jane’ }
];// Type is inferred as { id: number; name: string }[]
const names = users.map((u) => u.name);

5. Create Reusable Type Utilities

// types/common.ts
export type Nullable<T> = T | null;export type AsyncResult<T> = {
success: boolean;
data?: T;
error?: string;
};export type API<T> = Promise<AsyncResult<T>>;

// Usage in API calls
const getUser: API<User> = async (id) => {
// Implementation
};

✅ TypeScript Quality: Good TypeScript code is self-documenting. Types serve as inline documentation for how components and functions should be used.
© 2025 JavaScript UX. TypeScript for React Tutorial.

Leave a Reply

Your email address will not be published. Required fields are marked *