TypeScript with React
Why TypeScript with React?
React applications grow fast: components multiply, props evolve, and state becomes complex. TypeScript adds strong guarantees to your UI layer by making component APIs explicit. In production, this reduces bugs, improves refactoring confidence, and makes components easier to reuse correctly.
Core idea: components are contracts
In React, props are the public API of a component. TypeScript lets you define that API precisely, so incorrect usage is caught immediately by the editor and build pipeline.
Typing props in function components
Define a props type (or interface) and use it in the component signature.
type ButtonProps = {
label: string;
onClick: () => void;
disabled?: boolean;
};
function Button({ label, onClick, disabled = false }: ButtonProps) {
return (
);
}
Typing children
Many components accept children. Use React.ReactNode when you need to accept any renderable content.
type CardProps = {
title: string;
children: React.ReactNode;
};
function Card({ title, children }: CardProps) {
return (
{title}
{children}
);
}
Typing useState
TypeScript often infers state types, but explicit typing is helpful when the initial value is null or when the state is complex.
const [count, setCount] = React.useState(0); // inferred as number
type User = { id: number; name: string };
const [user, setUser] = React.useState(null);
Typing event handlers
React events are synthetic events. Use the correct event types for inputs and forms.
function SearchBox() {
const [q, setQ] = React.useState("");
function onChange(e: React.ChangeEvent) {
setQ(e.target.value);
}
return ;
}
Typing useRef
Refs can point to DOM elements or hold mutable values. DOM refs should include null because they are assigned after render.
const inputRef = React.useRef(null); function focus() { inputRef.current?.focus(); }
Typing async data and API responses
Model API responses as types and keep component state aligned. Always treat external data as untrusted and validate where necessary.
type User = { id: number; name: string };
async function fetchUsers(): Promise {
const res = await fetch("/api/users");
const data = await res.json();
return data as User[];
}
Discriminated unions for UI state
One of the best production patterns is modeling UI state with discriminated unions. This makes impossible states unrepresentable.
type LoadState =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: string[] }
| { status: "error"; message: string };
const [state, setState] = React.useState({ status: "idle" });
Props vs derived values
Avoid over-typing derived values. Type your boundaries (props, API, shared utils) and let inference handle internals. This keeps components readable while still safe.
Common mistakes
- Using any for props or API data to silence errors.
- Not typing nullable state and then assuming values exist.
- Overusing React.FC without understanding children and defaultProps implications.
- Forcing types with assertions instead of modeling the state correctly.
Production guidance
- Type component props explicitly (public API).
- Use discriminated unions for async/loading/error UI states.
- Prefer inference for local variables, be explicit at boundaries.
- Avoid any; use unknown + narrowing or runtime validation for external data.
- Keep types simple and readable for the team.
What’s next
Next, we should cover tooling and build workflow for real projects: scripts, watch mode, linting, and a practical migration strategy for adding TypeScript to existing React codebases.