TypeScript with React
Reviewed & published by Brayan K
TypeScript adds static types to JavaScript so mistakes are caught while you write code, not when a user hits a bug. In React, that means typing your props, state, and event handlers — the editor autocompletes them and the compiler refuses to ship a wrong shape. By the end you'll type a component end to end and read the most common React+TS patterns with confidence.
Part of the free React course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn
- Why static types catch React bugs early
- Type props with an interface or type alias
- Mark props optional and give defaults
- Type useState (inference vs. explicit generics)
- Use union types for status and variants
- Type event handlers and children correctly
1️⃣ Typing Props
The single most valuable thing TypeScript does for React: describe the shape of a component's props. Declare a type (or interface) and annotate the props parameter. Now every place that renders the component is checked — pass the wrong type or forget a required prop and you get a red squiggle immediately.
type ButtonProps = {
label: string;
onClick: () => void; // a function returning nothing
disabled?: boolean; // optional
};
function Button({ label, onClick, disabled = false }: ButtonProps) {
return <button onClick={onClick} disabled={disabled}>{label}</button>;
}
// ✅ ok: <Button label="Save" onClick={save} />
// ❌ error: <Button label={42} /> // 42 is not a string; missing onClickThe demo runs a typed function so you can see required and optional fields in action (the types are real TS in the block above):
// TypeScript lets you DESCRIBE the shape of your props so mistakes are
// caught before the app runs. Define a type, then annotate the parameter.
type ButtonProps = {
label: string; // required string
count: number; // required number
disabled?: boolean; // optional (the ? means "may be missing")
};
// Annotate the props object with the type:
function describeButton({ label, count, disabled = false }: ButtonProps): string {
const state = disabled ? "disabled" : "enabled";
return `[${label}] count=${count} (${state})`;
}
console.log(describeButton({ label: "Save", count: 3 }));
// [Save] count=3 (enabled)
console.log(describeButton({ label: "Delete", count: 0, disabled: true }));
// [Delete] count=0 (disabled)
// Pass the wrong type (e.g. count: "3") and TypeScript flags it at compile time.Your turn — fill in the type annotations so only valid data is accepted:
// 🎯 YOUR TURN — fill in each ___ then run it.
// Type this greet function's props so only valid data is accepted.
type UserProps = {
name: ___; // 👉 string (the user's name)
age: ___; // 👉 number (the user's age)
};
function greet({ name, age }: UserProps): string {
return `${name} is ${age}`;
}
console.log(greet({ name: "Ada", age: 36 }));
console.log(greet({ name: "Linus", age: 54 }));
// ✅ Expected output:
// Ada is 36
// Linus is 542️⃣ Typing State
Most of the time TypeScript infers state from the initial value: useState(0) is a number, useState("") is a string. You only annotate when the initial value doesn't tell the whole story — like null at first, or a union of options.
// Inference is enough — count is number, name is string:
const [count, setCount] = useState(0);
const [name, setName] = useState("");
// Annotate when the initial value is null or you need a union:
const [user, setUser] = useState<User | null>(null);
const [status, setStatus] = useState<"idle" | "loading" | "done">("idle");Union types shine for status flags and variants — the demo shows a Status union that makes typos impossible:
// Typing STATE and a discriminated union. A "Status" can only be one of
// three exact strings — TypeScript stops you from typo-ing "lodaing".
type Status = "idle" | "loading" | "success"; // a union of literal types
function label(status: Status): string {
switch (status) {
case "idle": return "Ready";
case "loading": return "Please wait…";
case "success": return "Done!";
}
}
console.log(label("idle")); // Ready
console.log(label("loading")); // Please wait…
console.log(label("success")); // Done!
// label("loadng") would be a COMPILE ERROR — the typo can't reach production.3️⃣ Events & Children
Two patterns you'll use constantly. Type an event handler with React's built-in event types so e.target.value is known. Type children with React.ReactNode — the catch-all for anything renderable.
// Event handler: React types the event so e.target.value is a string.
function onChange(e: React.ChangeEvent<HTMLInputElement>) {
console.log(e.target.value);
}
// Children: ReactNode covers JSX, strings, numbers, arrays, null...
type CardProps = { title: string; children: React.ReactNode };
function Card({ title, children }: CardProps) {
return <section><h2>{title}</h2>{children}</section>;
}A handy rule: for a value that returns nothing, type it () => void; for anything you can render, reach for React.ReactNode.
Reorder Challenge
These lines declare a typed component. Put them in a correct order:
A. return <button onClick={onClick}>{label}</button>;
B. type Props = { label: string; onClick: () => void };
C. function Button({ label, onClick }: Props) {
D. }Quick Recall
1) What does the ? in disabled?: boolean mean?
The prop is optional — it may be omitted. Inside the component its type is boolean | undefined.
2) Does useState(0) need a type annotation?
No. TypeScript infers number from the initial value. Annotate only for null starts or unions, e.g. useState<User | null>(null).
3) Which type covers "anything renderable" for the children prop?
React.ReactNode — it covers JSX, strings, numbers, arrays, null, and more.
Common Errors (and the fix)
- "Property 'x' is missing in type": you forgot a required prop. Add it, or mark it optional with ? if it really is.
- "Type 'string' is not assignable to type 'number'": you passed the wrong type. Fix the value, or widen the prop's type if appropriate.
- useState stuck on never or wrong type: the initial value was null. Add a generic: useState<Thing | null>(null).
- Typing the event as any: use the specific React event type (React.ChangeEvent<HTMLInputElement>) so e.target.value is known.
📋 Quick Reference
| Goal | Syntax |
|---|---|
| Props type | type Props = { label: string }; |
| Annotate props | function C({ label }: Props) {} |
| Optional prop | disabled?: boolean |
| State with union | useState<"a" | "b">("a") |
| Event handler | (e: React.ChangeEvent<HTMLInputElement>) => {} |
| Children | children: React.ReactNode |
Mini-Challenge: Type a Badge
Write a typed function that formats a badge. Type count as a number and an optional tone union, then return a label. Predict the output for the calls below.
🎉 Lesson Complete
- ✅ Types catch wrong props, state, and events before runtime
- ✅ Type props with a type/interface; ? marks optional
- ✅ useState usually infers; annotate for null or unions
- ✅ Union types make status flags typo-proof
- ✅ Type events with React's event types; children with ReactNode
Practice quiz
How do you type a component's props in modern React + TypeScript?
- Add PropTypes at the bottom of the file
- Wrap the component in a generic
- Define a type or interface for the props and annotate the props parameter
- Props can't be typed
Answer: Define a type or interface for the props and annotate the props parameter. Declare a type/interface (e.g. ButtonProps) and annotate the parameter: function Button(props: ButtonProps). Every usage is then checked.
What does the ? in disabled?: boolean mean?
- The prop is optional — it may be omitted, and its type becomes boolean | undefined
- The prop is private
- The prop defaults to true
- The prop is nullable but required
Answer: The prop is optional — it may be omitted, and its type becomes boolean | undefined. A ? marks the prop optional. Callers may omit it, and inside the component its type includes undefined.
Does useState(0) need an explicit type annotation?
- Yes, always annotate useState
- Only in strict mode
- Yes, or it defaults to any
- No — TypeScript infers number from the initial value
Answer: No — TypeScript infers number from the initial value. Inference handles primitives: useState(0) is number, useState('') is string. You annotate only when the initial value doesn't tell the whole story.
When do you need to annotate useState with a generic?
- Never
- When the initial value is null or you need a union type, e.g. useState<User | null>(null)
- Only for numbers
- Whenever the state is an object
Answer: When the initial value is null or you need a union type, e.g. useState<User | null>(null). A null start or a union of options can't be inferred fully, so you supply the generic: useState<User | null>(null) or useState<'idle' | 'done'>('idle').
What is a good use for union types like 'idle' | 'loading' | 'success'?
- Constraining a status to a fixed set of exact strings so typos become compile errors
- Counting renders
- Replacing useState
- Typing event handlers
Answer: Constraining a status to a fixed set of exact strings so typos become compile errors. A literal union restricts a value to specific strings. Mistyping 'lodaing' won't compile, catching the bug before runtime.
How should you type the children prop?
- string
- any
- React.ReactNode
- JSX.Element only
Answer: React.ReactNode. React.ReactNode is the catch-all for anything renderable: JSX, strings, numbers, arrays, null, and more.
How do you type an input's change handler so e.target.value is known?
- (e: any) => void
- (e: React.ChangeEvent<HTMLInputElement>) => void
- (e: Event) => void
- (e: string) => void
Answer: (e: React.ChangeEvent<HTMLInputElement>) => void. React's built-in event types describe the event. React.ChangeEvent<HTMLInputElement> makes e.target.value a known string.
What type best describes a callback prop that returns nothing, like onClick?
- () => null
- Function
- () => undefined required
- () => void
Answer: () => void. () => void types a function that takes no arguments and returns nothing — the standard shape for event-style callbacks.
Is React.FC the recommended way to type a component today?
- Yes, always use React.FC
- Not really — annotating the props object directly is the recommended modern style; React.FC adds quirks
- React.FC is required for hooks
- React.FC is the only option in strict mode
Answer: Not really — annotating the props object directly is the recommended modern style; React.FC adds quirks. Many teams skip React.FC because it adds an implicit children prop and some quirks. Annotating props directly (function Comp(props: Props)) is preferred.
You get "Property 'onClick' is missing in type". What does it mean?
- onClick is misspelled in React
- TypeScript is broken
- You forgot a required prop when rendering the component — add it or mark it optional with ?
- onClick must be a string
Answer: You forgot a required prop when rendering the component — add it or mark it optional with ?. The error flags a missing required prop. Either pass it, or mark it optional with ? if it genuinely is.
Continue this course
- Previous: React Router
- Next: Component Composition & children — Build flexible components with the children prop and composition over configuration
- Quick reference: React cheat sheet