TypeScript with React
Learn how TypeScript supercharges your React components by typing props, state, event handlers, and children — catching bugs before they ever hit the browser.
You already know how TypeScript catches mistakes in plain JavaScript files. Now imagine bringing that same safety net into a React component. Every prop your component accepts, every piece of state it holds, every button click it handles — TypeScript can verify all of it. The result is components that are easier to understand, impossible to misuse, and far less likely to blow up at runtime. We will work through four concrete skills: typing props with an interface, typing useState, typing event handlers, and typing children. By the end you will see why TypeScript and React are one of the most popular combinations in professional front-end development.
#Typing Props with an Interface
Props as a contract
Think of an interface like the printed label on a power adapter: it tells you exactly what voltage and plug shape is expected. If you try to plug in something that doesn't match, you know before you damage anything. An interface does the same job for your component — it documents the contract and enforces it at compile time.
interface UserCardProps {
name: string;
age: number;
isPremium?: boolean; // optional — the ? means it may be omitted
}
function UserCard({ name, age, isPremium = false }: UserCardProps) {
return (
<div>
<h2>{name}</h2>
<p>Age: {age}</p>
{isPremium && <span>Premium member</span>}
</div>
);
}#Typing useState
import { useState } from "react";
// TypeScript infers: count is number
const [count, setCount] = useState(0);
// Explicit type parameter — starts null, but will become a string later
const [username, setUsername] = useState<string | null>(null);
setUsername("Alice"); // OK
// setUsername(42); // Error: Argument of type 'number' is not assignableWhen to be explicit
If the initial value is null, undefined, [], or {}, TypeScript infers a very narrow type. Write an explicit type parameter — useState<User | null>(null) — so TypeScript knows the full range of values the state can hold.
#Typing Event Handlers
import { useState } from "react";
import type { ChangeEvent, MouseEvent } from "react";
function SearchBox() {
const [query, setQuery] = useState("");
function handleChange(event: ChangeEvent<HTMLInputElement>) {
setQuery(event.target.value); // TypeScript knows .value exists here
}
function handleClear(event: MouseEvent<HTMLButtonElement>) {
event.preventDefault();
setQuery("");
}
return (
<div>
<input value={query} onChange={handleChange} />
<button onClick={handleClear}>Clear</button>
</div>
);
}The element generic matters
ChangeEvent<HTMLInputElement> and ChangeEvent<HTMLSelectElement> are different types. Without the element generic, TypeScript gives you a very limited event object — event.target.value won't even be available. Always supply the HTML element type in angle brackets.
#Typing Children with ReactNode
import type { ReactNode } from "react";
interface CardProps {
title: string;
children: ReactNode; // accepts JSX, strings, numbers, null, arrays...
}
function Card({ title, children }: CardProps) {
return (
<section className="card">
<h3>{title}</h3>
<div className="card-body">{children}</div>
</section>
);
}
// <Card title="Welcome"><p>Any JSX can go here.</p></Card>import { useState } from "react";
import type { ReactNode, ChangeEvent } from "react";
interface FormFieldProps {
label: string;
children: ReactNode;
}
function FormField({ label, children }: FormFieldProps) {
return <label><span>{label}</span>{children}</label>;
}
function SignupForm() {
const [email, setEmail] = useState("");
function handleEmail(e: ChangeEvent<HTMLInputElement>) {
setEmail(e.target.value);
}
return (
<form>
<FormField label="Email">
<input type="email" value={email} onChange={handleEmail} />
</FormField>
<button type="submit">Sign up</button>
</form>
);
}You have a component that accepts a `count` prop (required number) and an `onReset` prop (optional function that takes no arguments and returns nothing). Which interface is correct?
Key takeaways
- Define a props interface above your component to document and enforce what callers must pass.
- Use `useState<YourType>(initialValue)` when the initial value doesn't reveal the full range of possible state.
- Type event handlers with `ChangeEvent<HTMLInputElement>` or `MouseEvent<HTMLButtonElement>` — always include the HTML element generic.
- Use `ReactNode` as the type for `children` in any wrapper component that accepts arbitrary JSX.
- TypeScript errors in React components appear in your editor before you run the code, so bugs never reach the browser.
Type annotations are erased at runtime. What does this code print?
import { useState } from "react";
const [username, setUsername] = useState<string | null>(null);
setUsername("Alice");
// pretend React re-runs and username is now "Alice"
const current: string | null = "Alice";
console.log(current?.toUpperCase() ?? "NO USER");This code has a bug — what's wrong?
import type { ChangeEvent } from "react";
function handleChange(event: ChangeEvent) {
console.log(event.target.value);
}
// used as: <input onChange={handleChange} />Complete the type for children so this wrapper component accepts any JSX, strings, numbers, or arrays — the widest renderable type from the lesson.
import type { } from "react"; interface CardProps { title: string; children: ; }
Put these lines in the correct order to define a props interface and a component that destructures those props — matching the lesson's UserCard style.
interface UserCardProps {}
return <h2>{name}</h2>;isPremium?: boolean;
function UserCard({ name, isPremium = false }: UserCardProps) {name: string;
Build a typed Badge component. It should accept a label (string, required), a color of either "blue", "green", or "red" (required), and an optional onClick handler (a function that returns void). Render a <span> that shows the label. Below it, write a BadgeDemo component that holds a count state (starts at 0) and renders a Badge with label Clicks: {count}, color "blue", and an onClick that increments the count.
Try it live — edit the code and hit Run to see the output: