Welcome! In this part we go slow and explain every concept you’ll touch daily in React + TypeScript. Every snippet has inline comments so you can copy, run, and learn.
0) How TypeScript helps in React
- Types = contracts between pieces of code (components, hooks, functions).
- TS is structural: if it looks like the type (has the right shape), it’s acceptable.
- Prefer inference (let TS figure it out) and add explicit types when needed (nulls, unions, generics).
Tip: Keep your editor’s TypeScript server running; errors are your friend during learning.
1) Typing Props (required, optional, default values, unions)
Props are just an object type that your component accepts.
// src/components/Greeting.tsx
// "type" and "interface" are equivalent for most component props. Pick one and be consistent.
type GreetingProps = {
name: string; // required prop
age?: number; // optional prop (may be undefined)
excited?: boolean; // optional flag
};
export function Greeting({ name, age = 0, excited = false }: GreetingProps) {
// age and excited now have defaults; their type is "number" and "boolean" respectively.
return (
<p>
Hello, <strong>{name}</strong>
{age ? ` (Age: ${age})` : ''}
{excited ? ' 🎉' : ''}
</p>
);
}
Usage (intentional errors shown as comments):
<Greeting name="Tushar" /> // ✅ age/excited are optional
<Greeting name="Tushar" age={25} /> // ✅
// <Greeting /> // ❌ Error: name is required
// <Greeting name={123} /> // ❌ Error: name must be string
Union props (e.g., a badge variant):
// src/components/Badge.tsx
type BadgeProps = {
text: string;
variant: 'info' | 'success' | 'error'; // union ensures only these literals are allowed
};
export function Badge({ text, variant }: BadgeProps) {
const color = variant === 'info' ? '#2563eb' // blue
: variant === 'success' ? '#16a34a' // green
: '#dc2626'; // red (error)
return <span style={{ padding: '2px 8px', background: color, color: '#fff' }}>{text}</span>;
}
2) Typing State (inference, nullable state, functional updates)
useState usually infers the type from the initial value. Add explicit types for nullable values or unions.
import { useState } from 'react';
export function Counter() {
const [count, setCount] = useState(0); // inferred as number
// Functional update keeps correctness when the new state depends on previous state.
const inc = () => setCount(prev => prev + 1);
const dec = () => setCount(prev => prev - 1);
return (
<div>
<button onClick={dec}>-</button>
<span style={{ margin: '0 8px' }}>{count}</span>
<button onClick={inc}>+</button>
</div>
);
}
Nullable state pattern:
// Example: user starts as null before login/Fetch
type User = { id: string; name: string };
export function Welcome() {
const [user, setUser] = useState<User | null>(null); // explicit union type
return <div>{user ? `Welcome, ${user.name}` : 'Welcome, guest'}</div>;
}
Avoid storing derived values in state:
// Bad: storing fullName when it can be computed
// Good:
const fullName = `${firstName} ${lastName}`; // derive during render instead of setState
3) Typing Events (mouse, change, submit, keyboard)
React wraps browser events with its own types under React.*Event.
Click (mouse) event:
function ClickMe() {
// e is a React.MouseEvent for a button element
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
// currentTarget is always the element the handler is bound to
console.log('clicked', e.currentTarget.name);
};
return <button name="primary" onClick={handleClick}>Click Me</button>;
}
Change event (input):
import { useState } from 'react';
function NameInput() {
const [name, setName] = useState('');
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setName(e.target.value); // target is the input element
};
return <input placeholder="Your name" value={name} onChange={handleChange} />;
}
Submit event (form):
function LoginForm() {
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault(); // prevent page reload
// read values via refs/state or FormData
};
return (
<form onSubmit={handleSubmit}>
<input name="email" type="email" />
<button type="submit">Login</button>
</form>
);
}
Keyboard event:
function HandleEnter() {
const onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
console.log('Enter pressed');
}
};
return <input onKeyDown={onKeyDown} placeholder="Press Enter" />;
}
Common pitfall: Mixing native
Eventwith React’s synthetic events. UseReact.ChangeEvent,React.MouseEvent, etc., in React code.
4) Typing Children (what can be rendered)
Use ReactNode to accept anything React can render.
import type { ReactNode } from 'react';
type CardProps = { children: ReactNode; title?: string };
export function Card({ children, title }: CardProps) {
return (
<section style={{ border: '1px solid #e5e7eb', padding: 16, borderRadius: 8 }}>
{title && <h3 style={{ marginTop: 0 }}>{title}</h3>}
{children}
</section>
);
}
If you frequently accept children, you can also use PropsWithChildren:
import type { PropsWithChildren } from 'react';
type PanelProps = PropsWithChildren<{ kind?: 'info' | 'warning' }>; // merges {children: ReactNode}
export function Panel({ kind = 'info', children }: PanelProps) {
return <div data-kind={kind}>{children}</div>;
}
5) Typing Refs (DOM elements & values)
Refs let you access DOM nodes or persist values across renders.
DOM ref:
import { useRef } from 'react';
export function FocusInput() {
// The ref can be null initially (before the input mounts)
const inputRef = useRef<HTMLInputElement>(null);
const focus = () => {
// Optional chaining guards against null during first render
inputRef.current?.focus();
};
return (
<div>
<input ref={inputRef} type="text" placeholder="Click the button to focus me" />
<button onClick={focus}>Focus</button>
</div>
);
}
Value ref (not tied to DOM):
import { useRef } from 'react';
function Stopwatch() {
// A mutable value that persists without causing re-renders
const startTimeRef = useRef<number | null>(null);
// ...assign startTimeRef.current = Date.now() when starting
return null;
}
Forwarding refs to a child component:
import { forwardRef } from 'react';
type TextInputProps = { label: string };
export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(
({ label }, ref) => {
return (
<label>
{label}
<input ref={ref} />
</label>
);
},
);
6) Narrowing with Discriminated Unions (exhaustive checks)
Model UI states with a tagged union and make impossible states impossible.
// One property ("kind") distinguishes each state shape
type LoadState =
| { kind: 'idle' }
| { kind: 'loading' }
| { kind: 'success'; data: string[] }
| { kind: 'error'; message: string };
export function DataStatus({ state }: { state: LoadState }) {
switch (state.kind) {
case 'idle':
return <p>Idle. Click fetch to start.</p>;
case 'loading':
return <p>Loading…</p>;
case 'success':
return (
<ul>
{state.data.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
);
case 'error':
return <p style={{ color: 'crimson' }}>Error: {state.message}</p>;
default: {
// If a new case is added to LoadState but not handled here,
// TypeScript will error below (exhaustiveness check):
const _never: never = state; // ❌ if a case is missing
return _never;
}
}
}
Pattern: add a
defaultbranch with anevervariable to catch unhandled cases during compilation.
7) Handy TS tips you’ll use daily
- Literal unions from arrays:
const statuses = ['idle', 'loading', 'success', 'error'] as const; type Status = typeof statuses[number]; // 'idle' | 'loading' | 'success' | 'error' satisfiesto validate object shapes without widening:const config = { env: 'dev', retry: 3, } as const satisfies { env: 'dev' | 'prod'; retry: number };- Avoid
any: it disables type safety. Prefer precise unions or generics. - Avoid
React.FCfor components (it forceschildrenand messes with generics). Type props yourself.
8) Practice (mini challenges)
- Create a
Togglecomponent with achecked: booleanprop and anonChange: (next: boolean) => voidprop. Type the event handler and add keyboard support (Space toggles). - Build a
Selectcomponent typed asHTMLSelectElementforonChange. Show the selected value. - Extend
LoadStatewith a"empty"case and make sure the switch stays exhaustive.
✅ Wrap-Up
You learned how to type props, state, events, children, refs, and how to model UI using discriminated unions with exhaustive checks. These patterns remove entire classes of bugs and make refactors safe.
Next: We’ll build reusable UI components (Button, Input, Modal) and introduce gentle generics to keep components flexible and type-safe. 🚀
Comments
Post a Comment