Chapter 23: TypeScript with React
TypeScript with React like we’re sitting together in a calm coding session in Hyderabad. No rush, step-by-step, whiteboard style — with real patterns people use in early 2026.
We’ll cover:
- Why TS + React is now the default for most serious frontend work
- Modern project setup (Vite vs Next.js — what most people choose in 2026)
- Typing components, props, state, events
- Hooks with full TypeScript (useState, useEffect, custom hooks)
- Common patterns & gotchas in 2026
- Real small examples you can copy-paste
Ready? Let’s go.
1. Why TypeScript + React in 2026?
Quick reality check (from State of JS / State of React surveys 2025):
- ~78–85% of professional React developers use TypeScript
- Almost every new job posting that mentions React also asks for TypeScript
- Big reasons:
- Catches ~70–90% of bugs before runtime (props mismatch, wrong hook usage, null/undefined crashes)
- Incredible autocompletion & refactoring in VS Code
- Self-documenting components (hover over prop → see exactly what it expects)
- Scales beautifully in teams of 5–500 people
- Works perfectly with modern tools (Vite, Next.js App Router, TanStack Query, Zustand, shadcn/ui, etc.)
2. Recommended setup in February 2026
Two dominant paths:
A. Vite + React + TypeScript (most popular for SPA / dashboard / internal tools / component libraries)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 |
# 1. Create project (official template) npm create vite@latest my-app -- --template react-ts cd my-app npm install # 2. Run npm run dev |
tsconfig.json (Vite generates very good defaults — usually just add paths if needed)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
{ "compilerOptions": { "target": "ES2022", "lib": ["ES2022", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true, "moduleResolution": "bundler", "allowImportingTsExtensions": true, "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "react-jsx", "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, "baseUrl": ".", "paths": { "@/*": ["./src/*"] } }, "include": ["src"], "references": [{ "path": "./tsconfig.node.json" }] } |
B. Next.js 14/15 + App Router + TypeScript (best for SEO, server components, full-stack apps)
|
0 1 2 3 4 5 6 7 |
npx create-next-app@latest my-next-app # Choose → Yes for TypeScript, Yes for Tailwind (optional), Yes for App Router |
Next.js auto-generates excellent tsconfig.json — rarely need to touch it.
Which to choose in 2026?
- SPA / admin panel / dashboard / design system → Vite + React-TS
- Marketing site / e-commerce / blog / public-facing app with SSR/SSG → Next.js + TypeScript
3. Typing Functional Components (the modern way)
In 2026 almost everyone uses functional components + hooks (class components are legacy).
Two main styles — both are correct:
Style 1: Explicit interface + FC (still very common)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
import React from 'react'; interface ButtonProps { label: string; onClick: () => void; disabled?: boolean; variant?: 'primary' | 'secondary' | 'danger'; size?: 'sm' | 'md' | 'lg'; } const Button: React.FC<ButtonProps> = ({ label, onClick, disabled = false, variant = 'primary', size = 'md', }) => { return ( <button onClick={onClick} disabled={disabled} className={`btn btn-{variant} btn-${size}`} > {label} </button> ); }; export default Button; |
Style 2: No FC — just function with props type (gaining popularity — cleaner children typing)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
type ButtonProps = { label: string; onClick: () => void; disabled?: boolean; children?: React.ReactNode; // now children is typed correctly }; function FancyButton({ label, onClick, disabled = false, children, }: ButtonProps) { return ( <button onClick={onClick} disabled={disabled}> {label} {children && <span className="ml-2">{children}</span>} </button> ); } |
Tip 2026: Many teams prefer Style 2 because React.FC auto-adds children?: ReactNode even when you don’t want it.
4. Typing Hooks — the bread & butter
useState
|
0 1 2 3 4 5 6 7 8 9 |
const [count, setCount] = useState<number>(0); // or inferred const [name, setName] = useState(''); // string const [user, setUser] = useState<User | null>(null); // when async / initial null |
useEffect
|
0 1 2 3 4 5 6 7 8 9 10 |
useEffect(() => { // fetch or subscription const timer = setInterval(() => {}, 1000); return () => clearInterval(timer); // cleanup always typed correctly }, [dependency]); |
useReducer (with discriminated unions — very powerful)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
type State = { count: number; error?: string }; type Action = | { type: 'increment' } | { type: 'decrement' } | { type: 'reset' }; const reducer = (state: State, action: Action): State => { switch (action.type) { case 'increment': return { ...state, count: state.count + 1 }; // ... default: return state; } }; const [state, dispatch] = useReducer(reducer, { count: 0 }); |
Custom Hook example (very common pattern)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
function useLocalStorage<T>(key: string, initialValue: T) { const [storedValue, setStoredValue] = useState<T>(() => { try { const item = window.localStorage.getItem(key); return item ? (JSON.parse(item) as T) : initialValue; } catch { return initialValue; } }); const setValue = (value: T | ((val: T) => T)) => { try { const valueToStore = value instanceof Function ? value(storedValue) : value; setStoredValue(valueToStore); window.localStorage.setItem(key, JSON.stringify(valueToStore)); } catch (error) { console.error(error); } }; return [storedValue, setValue] as const; } // Usage const [theme, setTheme] = useLocalStorage<'light' | 'dark'>('theme', 'light'); |
5. Typing Events & Forms
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
function InputDemo() { const [value, setValue] = useState(''); const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { setValue(e.target.value); }; const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); console.log(value); }; return ( <form onSubmit={handleSubmit}> <input type="text" value={value} onChange={handleChange} /> <button type="submit">Send</button> </form> ); } |
6. Quick 2026 best-practice cheat sheet
| Thing | Recommended way 2026 | Why / Note |
|---|---|---|
| Component type | Plain function + props type (avoid React.FC) | Better children control |
| State init null | useState<User |
null>(null) |
| Generic components | function List<T>({ items }: { items: T[] }) | Reusable table / card list |
| Event handlers | React.ChangeEvent<HTMLInputElement> | Full safety |
| useEffect deps | exhaustive-deps ESLint rule on | Catches missing deps |
| Custom hooks | Return tuple + as const | Preserve literal types |
| Children | children?: React.ReactNode | Or more specific ReactElement |
Your mini homework for today
- Create Vite + React-TS project (npm create vite@latest)
- Make a <UserCard user: { name: string; age?: number } /> component
- Add useState + useEffect to fetch mock users
- Intentionally pass wrong prop type → watch the red squiggle
Any part you want to zoom into deeper?
- Generic components with children
- Typing React Context
- Next.js App Router + Server Components + TS
- useReducer + Zustand / Redux Toolkit patterns
- Common ESLint + Prettier + TS setup
Just tell me — we’ll continue from exactly there! 😄
