TypeScript with React enables: IDE autocomplete, compile-time error detection, better documentation through types, improved refactoring safety, and self-documenting code.
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
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
├── 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
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:
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 | ✅ | ❌ |
Typing React Props
Basic Props Typing
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
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
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
title: string;
}export const Header: React.FC<HeaderProps> = ({ title }) => {
return <header><h1>{title}</h1></header>;
};
Typing State and Hooks
useState with Types
// 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
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
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
// 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
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
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)
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:
| { 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
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 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
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
{
“compilerOptions”: {
“strict”: true,
“noImplicitAny”: true,
“strictNullChecks”: true,
“strictFunctionTypes”: true
}
}
3. Organize Types in Separate Files
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
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
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
};