Typing Components
import React from 'react';
// Function component with props interface
interface GreetingProps {
name: string;
age?: number; // optional
}
function Greeting({ name, age }: GreetingProps) {
return (
<div>
Hello, {name}!
{age && <span> You are {age} years old.</span>}
</div>
);
}
// Arrow function component
const Welcome: React.FC<GreetingProps> = ({ name, age }) => {
return <h1>Welcome, {name}!</h1>;
};
// Component with children
interface CardProps {
title: string;
children: React.ReactNode;
}
function Card({ title, children }: CardProps) {
return (
<div className="card">
<h2>{title}</h2>
{children}
</div>
);
}
// Usage
<Card title="My Card">
<p>Card content</p>
</Card>
Typing Props
// Props with various types
interface ButtonProps {
label: string;
onClick: () => void;
variant?: 'primary' | 'secondary' | 'danger';
disabled?: boolean;
icon?: React.ReactNode;
className?: string;
}
function Button({
label,
onClick,
variant = 'primary',
disabled = false,
icon,
className
}: ButtonProps) {
return (
<button
onClick={onClick}
disabled={disabled}
className={`btn btn-${variant} ${className || ''}`}
>
{icon && <span className="icon">{icon}</span>}
{label}
</button>
);
}
// Props extending HTML attributes
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
label: string;
error?: string;
}
function Input({ label, error, ...inputProps }: InputProps) {
return (
<div>
<label>{label}</label>
<input {...inputProps} />
{error && <span className="error">{error}</span>}
</div>
);
}
// Render props pattern
interface DataFetcherProps<T> {
url: string;
render: (data: T | null, loading: boolean) => React.ReactNode;
}
Typing Hooks
import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
// useState with type inference
const [count, setCount] = useState(0); // inferred as number
// useState with explicit type
const [user, setUser] = useState<User | null>(null);
// useState with complex state
interface FormState {
name: string;
email: string;
age: number;
}
const [form, setForm] = useState<FormState>({
name: '',
email: '',
age: 0
});
// useRef
const inputRef = useRef<HTMLInputElement>(null);
const countRef = useRef<number>(0);
// useCallback with typed parameters
const handleClick = useCallback((id: number) => {
console.log('Clicked:', id);
}, []);
// useMemo with return type
const expensiveValue = useMemo<number>(() => {
return someExpensiveCalculation();
}, [dependency]);
// useEffect with cleanup
useEffect(() => {
const handler = (e: KeyboardEvent) => {
console.log(e.key);
};
window.addEventListener('keydown', handler);
return () => {
window.removeEventListener('keydown', handler);
};
}, []);
Custom Hooks
// Custom hook with generics
function useLocalStorage<T>(key: string, initialValue: T) {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
const setValue = (value: T | ((val: T) => T)) => {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
localStorage.setItem(key, JSON.stringify(valueToStore));
};
return [storedValue, setValue] as const;
}
// Usage
const [name, setName] = useLocalStorage<string>('name', '');
// Fetch hook
interface UseFetchResult<T> {
data: T | null;
loading: boolean;
error: Error | null;
}
function useFetch<T>(url: string): UseFetchResult<T> {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
fetch(url)
.then(res => res.json())
.then(setData)
.catch(setError)
.finally(() => setLoading(false));
}, [url]);
return { data, loading, error };
}
// Usage
const { data: users, loading } = useFetch<User[]>('/api/users');
Event Handling
// Mouse events
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
console.log(e.currentTarget);
};
// Form events
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
// handle form submission
};
// Input events
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setName(e.target.value);
};
// Select events
const handleSelect = (e: React.ChangeEvent<HTMLSelectElement>) => {
setOption(e.target.value);
};
// Keyboard events
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
handleSubmit();
}
};
// Focus events
const handleFocus = (e: React.FocusEvent<HTMLInputElement>) => {
e.target.select();
};
// Drag events
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
const files = e.dataTransfer.files;
};
Context API
import { createContext, useContext, useState, ReactNode } from 'react';
// Define context type
interface ThemeContextType {
theme: 'light' | 'dark';
toggleTheme: () => void;
}
// Create context with default value
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
// Provider component
interface ThemeProviderProps {
children: ReactNode;
}
function ThemeProvider({ children }: ThemeProviderProps) {
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 context
function useTheme(): ThemeContextType {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}
// Usage
function ThemedButton() {
const { theme, toggleTheme } = useTheme();
return (
<button onClick={toggleTheme}>
Current theme: {theme}
</button>
);
}
Generic Components
// Generic list component
interface ListProps<T> {
items: T[];
renderItem: (item: T, index: number) => React.ReactNode;
keyExtractor: (item: T) => string;
}
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
return (
<ul>
{items.map((item, index) => (
<li key={keyExtractor(item)}>
{renderItem(item, index)}
</li>
))}
</ul>
);
}
// Usage
interface User {
id: string;
name: string;
}
const users: User[] = [
{ id: '1', name: 'Alice' },
{ id: '2', name: 'Bob' }
];
<List
items={users}
renderItem={(user) => <span>{user.name}</span>}
keyExtractor={(user) => user.id}
/>
Key Takeaways
- • Define interfaces for component props
- • Use generic types for hooks like useState
- • Type event handlers with React event types
- • Create type-safe custom hooks
- • Use generic components for reusability
- • Type Context API with proper null checks
Typed Refs and Discriminated Union State
Use useRef with the correct DOM element type for full IntelliSense. Use discriminated unions to make impossible states unrepresentable.
// Typed DOM refs
const inputRef = useRef<HTMLInputElement>(null);
const focusInput = () => inputRef.current?.focus(); // null-safe access
// Forward refs
const Button = React.forwardRef<HTMLButtonElement, { label: string }>(
({ label }, ref) => <button ref={ref}>{label}</button>
);
// Discriminated union for async state — TypeScript narrows each branch
type FetchState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error };
function DataView<T>({ state }: { state: FetchState<T> }) {
switch (state.status) {
case 'idle': return <p>No data yet</p>;
case 'loading': return <p>Loading...</p>;
case 'error': return <p>Error: {state.error.message}</p>;
case 'success': return <pre>{JSON.stringify(state.data)}</pre>;
}
// state.data is only accessible in 'success' — no type assertions needed
}
📚 Continue Learning
Apply TypeScript across your full-stack projects:
React Tutorial
Build type-safe React components with hooks, props, and context.
Next.js Tutorial
Full-stack TypeScript with App Router, Server Components, and API routes.
Node.js Tutorial
Build type-safe server-side applications with TypeScript and Node.
Advanced JavaScript
Understand the JS runtime that TypeScript compiles down to.