TS with React (Props & State)
React components with TypeScript — every prop, state, and event is checked at the gate.
React and TypeScript are a perfect match. React's component-based architecture maps naturally to TypeScript's type system — every component has a clear contract of what props it accepts, what state it manages, and what events it handles. TypeScript enforces these contracts at compile time, catching mismatches before they reach the browser.
Think of a Charminar-level quality check for your components. Every component is like a shop in the Laad Bazaar around Charminar — each shop has a specific inventory, serves specific customers, and operates in a specific way. If a customer walks in expecting bangles at a shop that only sells pearls, there's a mismatch — the shop's "props" and the customer's "expectations" don't align.
TypeScript is like having a quality inspector at every shop's entrance. The inspector checks that the customer brings the right items (props), understands what the shop offers (component API), and doesn't ask for things that don't exist (invalid state). Before the transaction happens, the inspector catches the mismatch. No runtime crashes, no confused customers, no broken workflows.
In this chapter, we'll cover the essential React + TypeScript patterns: typing props (the shop inventory), typing state (what's in the back room), typing events (customer interactions), and typing hooks (the shop's tools). Once you learn these patterns, every component you write will be self-documenting, type-safe, and impossible to misuse — like a well-organized shop in Hyderabad's most famous market.
The foundation of any React + TypeScript component is the props interface. This defines the contract between the parent component (who uses the component) and the child component (who renders the UI).
Basic Props Interface
// Define the props contract
interface ButtonProps {
label: string;
onClick: () => void;
disabled?: boolean;
variant: "primary" | "secondary";
}
// Define the component
const Button: React.FC<ButtonProps> = ({
label,
onClick,
disabled = false,
variant,
}) => {
const className =
`btn btn-${variant}`;
return (
<button
className={className}
onClick={onClick}
disabled={disabled}
>
{label}
</button>
);
};
Alternative: Direct Prop Annotation
// Some teams prefer this style
// (no implicit children prop)
function Button({
label,
onClick,
variant,
}: ButtonProps) {
return (
<button onClick={onClick}>
{label}
</button>
);
}
Children Prop
interface CardProps {
title: string;
children: React.ReactNode;
}
// children can be:
// string, number, JSX, arrays, fragments
// ReactNode covers all of these
function Card({ title, children }: CardProps) {
return (
<div className="card">
<h2>{title}</h2>
{children}
</div>
);
}
Generic Components
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
}
function List<T>({
items,
renderItem
}: ListProps<T>) {
return (
<ul>
{items.map(renderItem)}
</ul>
);
}
// Usage — T is inferred!
<List
items={users}
renderItem={(user) =>
<li>{user.name}</li>
}
/>TypeScript also provides full type safety for React state management and event handling.
useState with TypeScript
// Type is INFERRED from initial value
const [count, setCount] = useState(0);
// count is number, setCount expects number
const [name, setName] = useState("");
// name is string
const [isActive, setIsActive] =
useState(false);
// isActive is boolean
// EXPLICIT type for complex state
interface User {
id: number;
name: string;
}
const [user, setUser] =
useState<User | null>(null);
// Must specify union type
const [items, setItems] =
useState<string[]>([]);
// Explicit for empty arrays
useReducer with TypeScript
type Action =
| { type: "increment" }
| { type: "decrement" }
| { type: "reset" }
| { type: "set"; payload: number };
function reducer(
state: number,
action: Action
): number {
switch (action.type) {
case "increment":
return state + 1;
case "decrement":
return state - 1;
case "reset":
return 0;
case "set":
return action.payload;
}
}
const [state, dispatch] =
useReducer(reducer, 0);
Event Handlers
// Click events
const handleClick = (
e: React.MouseEvent<
HTMLButtonElement
>
) => {
console.log(e.clientX, e.clientY);
};
// Change events
const handleChange = (
e: React.ChangeEvent<
HTMLInputElement
>
) => {
console.log(e.target.value);
};
// Form submit
const handleSubmit = (
e: React.FormEvent<
HTMLFormElement
>
) => {
e.preventDefault();
// Process form data
};
useRef with TypeScript
// Mutable ref (no initial value)
const intervalRef =
useRef<number | null>(null);
// DOM element ref
const inputRef =
useRef<HTMLInputElement>(null!);
// Use null! only when you know
// the element exists on mount
// Safer approach:
const inputRef =
useRef<HTMLInputElement>(null);
useEffect(() => {
inputRef.current?.focus();
// Optional chaining handles null
}, []);React + TypeScript has some common pitfalls. Let's walk through the most important ones to avoid.
Trap 1: Not handling null state
const [user, setUser] =
useState<User>(null!);
// null! lies to TypeScript!
// At runtime, user might be null
// CORRECT:
const [user, setUser] =
useState<User | null>(null);
if (user) {
console.log(user.name);
}
Trap 2: Over-typing with React.FC
React.FC includes implicit children. If your component doesn't accept children, prefer direct prop annotation to be explicit.
Trap 3: Using typeof useState incorrectly
// This does NOT work:
const [items, setItems] =
useState([]);
// items is never[] — empty array type!
// Must specify:
const [items, setItems] =
useState<string[]>([]);
Trap 4: Forgetting that setState with objects needs spread
const [form, setForm] =
useState({ name: "", email: "" });
// setState does NOT merge!
// Must spread manually:
setForm(prev => ({
...prev,
name: "Imran",
}));Here's your complete cheatsheet for React + TypeScript!
Component Types:
interface Props { name: string; }
const Comp: React.FC<Props> = ({ name }) => ...
function Comp({ name }: Props) { ... }
function Comp<T>({ items }: { items: T[] }) => ...
Hooks:
useState<T>(initial)
useReducer<S, A>(reducer, initial)
useRef<T>(initial)
useContext<T>(context)
Events:
React.MouseEvent<E>
React.ChangeEvent<E>
React.FormEvent<E>
React.KeyboardEvent<E>
Key Rules:
- Always define a props interface for every component — this is the contract
- Handle null/undefined in state explicitly with union types
- Use React event types (not DOM event types) for handlers
- Specify generic type for empty arrays: useState<string[]>([])
- Use useReducer for complex state logic with discriminated unions
The Golden Rule: "React + TypeScript is like a Charminar-level quality check. Every prop is inspected, every state is verified, every event is validated. The compiler catches mismatches before they reach your users. Treat your components like Laad Bazaar shops — clear contracts, consistent products, and zero surprises, bhai!"
Key Takeaways
- Define a props interface for every component — it serves as the component contract
- useState infers type from initial value; use explicit type for null or empty array initial values
- Use React synthetic event types (React.MouseEvent, etc.) instead of DOM event types
- useReducer with discriminated unions provides type-safe state management
- Always use optional chaining (?.) or null checks for optional props and nullable state
- Prefer direct prop annotation over React.FC to avoid implicit children prop
Want to track your progress?
Log in to save your place and pick up where you left off.
Progress track karna chahte ho?
Login karo apni progress save karne ke liye aur jahan chhoda tha wahan se shuru karo.
Login