JavaScript catches errors at runtime — when the program is already running for the user. TypeScript catches errors earlier: while you are writing code in the editor. This changes how development feels: instead of "run and check," you get an instant hint right in the code.
In React, TypeScript is especially useful in a few places: component props, events, state, and references to DOM elements. Let's go through each one.
Why TypeScript if you already have JavaScript
In plain JavaScript, a component accepts anything. Say there's a button:
function Button({ variant, onClick, children }) {
return <button className={variant} onClick={onClick}>{children}</button>;
}
Nothing stops you from calling it like this: <Button variant="primry" /> — a typo in the variant name, onClick not passed. JavaScript will stay silent. The user will see a broken button.
TypeScript adds a description of what the component expects. Pass something wrong — the editor underlines the error before the code even runs. This is static typing: checking before execution, not during.
Typing props
Props are the data a parent passes to a component. They are described using type or interface. In practice, type is more commonly used — it is slightly more flexible.
type ButtonProps = {
variant: "primary" | "secondary";
disabled?: boolean;
onClick: () => void;
children: React.ReactNode;
};
function Button({ variant, disabled = false, onClick, children }: ButtonProps) {
return (
<button className={variant} disabled={disabled} onClick={onClick}>
{children}
</button>
);
}
A few details:
"primary" | "secondary"— instead of a plainstring. Nowvariant="primry"won't compile, and the editor will suggest the valid options.disabled?— the question mark means the prop is optional.React.ReactNode— the type forchildren: accepts text, elements, arrays, andnull.
Reusable components and generics
Imagine a list of products. Then a list of orders. Then a list of users. Writing a separate list component each time is inconvenient — it's easier to have one that works with any data.
For this, TypeScript offers generics. The letter T in angle brackets means "whatever type the caller will provide":
type ListProps<T> = {
items: T[];
renderItem: (item: T) => React.ReactNode;
keyOf: (item: T) => string;
};
function List<T>({ items, renderItem, keyOf }: ListProps<T>) {
return (
<ul>
{items.map((item) => (
<li key={keyOf(item)}>{renderItem(item)}</li>
))}
</ul>
);
}
When we use List<Product>, TypeScript knows: renderItem receives a Product, not just anything. If you try to access a non-existent field — an error in the editor, not in the browser.
States with multiple variants
Here's a common situation: a component loads data from the server. While loading — a spinner; if there's an error — a message; if ready — the data. How do you model this?
The first impulse is a set of optional flags:
type State = {
isLoading?: boolean;
error?: string;
data?: Product;
};
The problem: this type allows nonsensical combinations — for example, isLoading: true and data: {...} at the same time. TypeScript won't object, and the component will behave unpredictably.
The solution is a discriminated union: a union of types with a shared tag field (status) that lets TypeScript know exactly which state you're in.
type RemoteData<T> =
| { status: "loading" }
| { status: "error"; error: string }
| { status: "success"; data: T };
function ProductView({ state }: { state: RemoteData<Product> }) {
switch (state.status) {
case "loading": return <Spinner />;
case "error": return <ErrorBox message={state.error} />;
case "success": return <ProductCard product={state.data} />;
}
}
In each branch of the switch, TypeScript knows exactly which fields are available: in the "success" branch there is state.data, in "error" — state.error. Trying to access a field from another branch is a compile error.
Typing events and ref
React events have specific types. The type depends on which element the event occurs on and which handler it is.
function SearchInput() {
const inputRef = useRef<HTMLInputElement>(null);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value);
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
inputRef.current?.focus();
};
return (
<form onSubmit={handleSubmit}>
<input ref={inputRef} onChange={handleChange} />
</form>
);
}
Useful event types:
React.ChangeEvent<HTMLInputElement>— input field changeReact.FormEvent<HTMLFormElement>— form submissionReact.MouseEvent<HTMLButtonElement>— button click
useRef<HTMLInputElement>(null) — a typed reference to a DOM element. The editor will suggest all available methods and properties of the element.
Derived types: don't repeat yourself
TypeScript lets you build new types from existing ones without copying them. Three commonly used techniques:
type Product = {
id: string;
name: string;
price: number;
createdAt: string;
};
// Creation form: everything except server-generated fields
type CreateProductInput = Omit<Product, "id" | "createdAt">;
// Edit form: all fields are optional
type UpdateProductInput = Partial<Product>;
// Name and price only, for a preview card
type ProductPreview = Pick<Product, "name" | "price">;
The point: if Product changes, derived types update automatically. One source of truth instead of manual synchronization.
In short
- TypeScript checks types before running — errors are visible in the editor, not to the user.
- Props are described with
typeorinterface; narrow string literal unions instead ofstringcatch typos. - Reusable components (list, table, select) are made generic with
<T>so types are not lost. - Mutually exclusive states are described with a discriminated union with a tag field — this eliminates impossible combinations.
- The event type depends on the element:
React.ChangeEvent<HTMLInputElement>,React.MouseEvent<HTMLButtonElement>, etc. useRef<HTMLElement>(null)— a typed reference to a DOM node.Omit,Partial,Pick— build new types from existing ones without duplication.
What to read next
- Hooks: custom hooks and useEffect pitfalls — how to type custom hooks.
- Forms and validation — types for form fields and validation schemas.
- Project structure and components — how to organize typed components in a project.