cf.completefrontend
Open lab My progress
THE TYPESCRIPT FIELD GUIDE

Typing React components

Type a React component's props so every tag that uses it is checked: children and ReactNode, event handlers, callback props, and optional props with defaults.

By the end, you can
  • 01
    Type props and childrenWrite a props type, take it apart in the parameter list, and type children as ReactNode.
  • 02
    Type event handlersLet inline handlers get their event type, and give handlers of their own types like ChangeEvent<HTMLInputElement>.
  • 03
    Make props optionalMark props with ?, give them default values, and pass functions down as typed callback props.

A component that says what it needs

React builds a page out of components. A component is a function that takes one object, called its props, and returns JSX: tags that look like HTML and describe what to show. When you write <CartItem name="Tea" price={3} />, React calls CartItem with the props object { name: "Tea", price: 3 }. Quotes pass text. Braces pass any JavaScript value, like the number 3.

Here is a small cart list in plain JavaScript with JSX, with one mistake in it:

A cart list in plain JSXPop out in the code editor (opens in a new tab)JSX
import { createRoot } from "react-dom/client";

function CartItem({ name, price }) {
  return <li>{name}: ${price.toFixed(2)}</li>;
}

createRoot(document.getElementById("root")).render(
  <ul>
    <CartItem name="Tea" price={3} />
    <CartItem name="Cake" price="4" />
  </ul>
);

Line 3 makes the CartItem component. Its one argument is the props object, and the braces take name and price out of it (Destructuring explains this). Line 4 shows the name and the price with two decimals. The $ there is just a dollar sign in the text. Line 7 tells React to draw the list into the page's <div id="root">. Line 9 passes the price 3 in braces, a number. Line 10 passes "4" in quotes, which is text. Text has no toFixed method, so the code stops with TypeError: price.toFixed is not a function, and the page shows nothing. Nothing warned you before it ran.

With TypeScript, you give the props a type. The file ends in .tsx, which means TypeScript with JSX:

The same cart with a props type (an error on purpose)Pop out in the code editor (opens in a new tab)TSX
import { createRoot } from "react-dom/client"; type CartItemProps = {  name: string;  price: number;}; function CartItem({ name, price }: CartItemProps) {  return <li>{name}: ${price.toFixed(2)}</li>;} createRoot(document.getElementById("root")!).render(  <ul>    <CartItem name="Tea" price={3} />    <CartItem name="Cake" price="4" />  </ul>);

Lines 3 to 6 describe the props: a name that is a string and a price that is a number. Line 8 puts : CartItemProps after the props, the way you type any function parameter. Line 12 adds a ! after getElementById, because that call can return null (Typing the DOM explains why). TypeScript marks line 15, on purpose here, because it's the bug:

  • Type 'string' is not assignable to type 'number' means "you're passing text where CartItemProps says a number goes". TypeScript checks every tag that uses CartItem, before the code runs.

Change what line 15 passes and watch what TypeScript and the page say:

Playground: what the Cake item gets
A cart of tea and cakePop out in the code editor (opens in a new tab)TSX
import { createRoot } from "react-dom/client"; type CartItemProps = {  name: string;  price: number;}; function CartItem({ name, price }: CartItemProps) {  return <li>{name}: ${price.toFixed(2)}</li>;} createRoot(document.getElementById("root")!).render(  <ul>    <CartItem name="Tea" price={3} />    <CartItem name="Cake" price="4" />  </ul>);
Line 15 passesprice="4"
What happens
TypeScript1 error: line 15
The pageStops: TypeError: price.toFixed is not a function
Try it yourself

Quotes pass text, so TypeScript says Type 'string' is not assignable to type 'number'. When it runs, text has no toFixed method, so React stops and the list stays empty.

TypeScript checks the code for real, and "The page" is what React shows when the code runs. Each button changes only line 15. Reset goes back to price="4".

Only price={4} passes the check. The typo prise and the text "4" would break the page, and TypeScript catches both. The extra onSale wouldn't break anything, but TypeScript still flags it, because CartItemProps doesn't list it. A prop the component doesn't know about is usually a mistake.

Left, what the tag on line 15 passes: { name: "Cake", price: "4" }. Right, what CartItemProps asks for: { name: string; price: number }. Arrows match name: "Cake" to name: string, marked fits, and price: "4" to price: number, marked error.the tag on line 15 passes{ name: "Cake", price: "4" }CartItemProps asks for{ name: string; price: number }name: "Cake"name: stringfitsprice: "4"price: numbererror
TypeScript turns the attributes on <CartItem> into one props object and checks it against CartItemProps. The name fits. The price is text where a number has to go, so line 15 is marked.
Real-life analogyA cake order form

A bakery takes cake orders on a printed form. It has a box for the name to write on the cake and a box for the number of candles. The clerk reads the form at the counter. If someone writes "four" in the Candles box, or leaves out the name, the clerk asks them to fix it right there. The baker in the back only ever gets forms that make sense.

In real life: The bakery's order form, with a box for each detail
In TypeScript: The props type, with a type for each prop
In real life: A customer fills in the form
In TypeScript: A tag passes attributes to the component
In real life: The clerk sees "four" written in the Candles box and asks for a number
In TypeScript: TypeScript marks price="4" where a number goes
In real life: The baker works from a form that was checked
In TypeScript: The component runs with props of the right types
The main idea

Give each component a props type. TypeScript then checks every tag that uses the component against it, while you type: a missing prop, a misspelled one, or a value of the wrong type is marked before the page can break.

Where React's types come from

In a project, three things make this work. Name the file .tsx. Set "jsx": "react-jsx" in tsconfig.json, which tells TypeScript to read JSX the way React's build tools do. Install React's types with npm install --save-dev @types/react @types/react-dom. Templates from Vite and Next.js do all three for you.

The examples on this page are checked against a short copy of React's types, with only the parts this lesson uses, and the lesson's tests check every example against the full @types/react too. When you press Edit & run, the real React loads from esm.sh, so the examples draw on a real page.

Typing a component's props

A component always gets exactly one argument: the props object. So you type it like any other function parameter (Typing functions covers parameters). Here the props type is an interface:

A profile card with a props interfacePop out in the code editor (opens in a new tab)TSX
import { createRoot } from "react-dom/client"; interface ProfileCardProps {  name: string;  city: string;  orders: number;} function ProfileCard(props: ProfileCardProps) {//                   ^?  return <p>{props.name} from {props.city} has {props.orders} orders</p>;} createRoot(document.getElementById("root")!).render(  <ProfileCard name="Asha" city="Pune" orders={3} />);

Lines 3 to 7 list the three props. Line 9 says the parameter props has that type, and the query on line 10 shows it: (parameter) props: ProfileCardProps. Line 11 reads each prop from the object. Line 15 passes them all, with orders={3} in braces because it's a number. The page shows Asha from Pune has 3 orders.

Most React code takes the props apart right in the parameter list. The type still goes after the whole pattern, not after each name:

The same card with its props taken apartPop out in the code editor (opens in a new tab)TSX
import { createRoot } from "react-dom/client"; interface ProfileCardProps {  name: string;  city: string;  orders: number;} function ProfileCard({ name, city, orders }: ProfileCardProps) {//       ^?  return <p>{name} from {city} has {orders} orders</p>;} createRoot(document.getElementById("root")!).render(  <ProfileCard name="Asha" city="Pune" orders={3} />);

Line 9 takes name, city, and orders out of the props, and : ProfileCardProps after the braces types all three. The query on line 10 points at the function's name and shows function ProfileCard(…): JSX.Element. You never wrote a return type. TypeScript worked it out from the JSX on line 11: JSX.Element is React's type for what a tag like <p> makes. You rarely need to write it yourself.

Props types can use everything you know about types. A prop that takes one of a few words is a union of literal types:

A stock badge with a word that isn't allowed (an error on purpose)Pop out in the code editor (opens in a new tab)TSX
import { createRoot } from "react-dom/client"; type StockBadgeProps = {  status: "in-stock" | "low" | "sold-out";}; const labels = { "in-stock": "In stock", low: "Only a few left", "sold-out": "Sold out" }; function StockBadge({ status }: StockBadgeProps) {  return <span>{labels[status]}</span>;} createRoot(document.getElementById("root")!).render(  <p>    Green tea: <StockBadge status="low" />. Lemon cake: <StockBadge status="soldout" />.  </p>);

Line 4 says status must be one of three words. Line 10 uses it to look up a label. Line 15 passes "low", which is fine, and then "soldout", which TypeScript marks on purpose:

  • Type '"soldout"' is not assignable to type '"in-stock" | "low" | "sold-out"'. Did you mean '"sold-out"'? means the word isn't one of the three, and TypeScript found the one you probably meant.

Without TypeScript, nothing would complain: labels["soldout"] is undefined, so the page shows Lemon cake: . with an empty badge, and a shopper never learns the cake is gone.

You'll also see components typed with FC (short for function component) from React's types. It types the whole function at once:

A component typed with FCPop out in the code editor (opens in a new tab)TSX
import { createRoot } from "react-dom/client";import type { FC } from "react"; type GreetingProps = { name: string }; const Greeting: FC<GreetingProps> = ({ name }) => <h2>Welcome back, {name}</h2>; createRoot(document.getElementById("root")!).render(<Greeting name="Asha" />);

Line 2 imports the type FC. import type brings in types only, so the line is removed before the code runs (TypeScript modules explains it). Line 6 says Greeting is an FC whose props are GreetingProps, so name is a string with no type written next to it. Line 8 draws Welcome back, Asha. The table compares the two ways. Read each row as a question, then each way's answer.

A plain function or FC?
The questionA plain functionFC
How does it look?function Greeting({ name }: GreetingProps)const Greeting: FC<GreetingProps> = ({ name }) => …
What does TypeScript check at each tag?The props, against GreetingProps.The props, against GreetingProps. The same check.
How is the return checked?TypeScript works it out from what you return. Each tag that uses it checks that React can draw it.Where you write the function: it must be something React can draw.
Does it accept children?Only if GreetingProps lists children.Only if GreetingProps lists children.

Children and ReactNode

Whatever you put between a component's opening and closing tags arrives as a prop named children. TypeScript checks it like any other prop, so your props type has to list it:

A panel that wraps whatever is inside itPop out in the code editor (opens in a new tab)TSX
import { createRoot } from "react-dom/client";import type { ReactNode } from "react"; type PanelProps = {  title: string;  children: ReactNode;}; function Panel({ title, children }: PanelProps) {  return (    <section>      <h2>{title}</h2>      {children}    </section>  );} createRoot(document.getElementById("root")!).render(  <Panel title="Your cart">    <p>Tea × 2</p>  </Panel>);

Line 2 imports the type ReactNode from React. Line 6 says children is a ReactNode. Line 13 draws the children under the heading. Lines 19 to 21 use the panel: the paragraph on line 20 sits between the tags, so it becomes children. The page shows the heading Your cart and then Tea × 2.

ReactNode is React's name for anything it can draw:

  • an element, like <p>Tea × 2</p>,
  • text and numbers,
  • an array of any of these, like a list from .map(),
  • null, undefined, true, and false, which draw nothing.
Left, a crate labelled ReactNode holds five cards: <p>Tea × 2</p>, a string of text, the number 2, null, and an array. Right, outside the crate, a card { items: 2 } with the note: an object is not a ReactNode.fits in children: ReactNode<p>Tea × 2</p>"Empty cart"2null[<li />, <li />]ReactNodedoesn't fit{ items: 2 }not a ReactNode
ReactNode is everything React can draw: elements, text, numbers, arrays of those, and null, undefined, or true/false, which draw nothing. A plain object isn't one of them.

Try each one between the tags, and one thing that isn't a ReactNode:

Playground: what goes inside the panel
A panel with something insidePop out in the code editor (opens in a new tab)TSX
import { createRoot } from "react-dom/client";import type { ReactNode } from "react"; type PanelProps = {  title: string;  children: ReactNode;}; function Panel({ title, children }: PanelProps) {  return (    <section>      <h2>{title}</h2>      {children}    </section>  );} const cart = { items: 2, total: 6 };createRoot(document.getElementById("root")!).render(  <Panel title="Your cart">    <p>Tea × 2</p>  </Panel>);
Between the tags<p>Tea × 2</p>
What happens
TypeScriptNo errors
The pageShows Your cart, then Tea × 2
Try it yourself

An element is a ReactNode, so the panel takes it. React draws it under the heading.

TypeScript checks the code for real, and the page is what React shows. Each button changes only what sits between the Panel tags. Reset goes back to the paragraph.

The element, the text, the number, and null are all fine. The object cart is caught twice: by TypeScript while you type, and by React, which throws Objects are not valid as a React child when it runs. An empty panel is an error only because line 6 makes children required.

ReactNode is the right type for most children. Two other types from React come up too: PropsWithChildren, which adds an optional children to a props type, and ReactElement, which allows only an element, not text:

Optional children, and children that must be an elementPop out in the code editor (opens in a new tab)TSX
import { createRoot } from "react-dom/client";import type { PropsWithChildren, ReactElement } from "react"; type NoteProps = PropsWithChildren<{ tone: "info" | "warning" }>; function Note({ tone, children }: NoteProps) {//                    ^?  return <p className={tone}>{children}</p>;} function Frame({ children }: { children: ReactElement }) {  return <div className="frame">{children}</div>;} createRoot(document.getElementById("root")!).render(  <Frame>    <Note tone="info">Free delivery over $20</Note>  </Frame>);

Line 4 makes the props tone plus an optional children. The query on line 7 shows children: React.ReactNode. Line 11 types Frame's children as ReactElement. Lines 16 to 18 put a Note element inside the frame, which fits. The page shows Free delivery over $20. Put the text straight inside the frame instead, and TypeScript objects:

Text inside a frame that wants an element (an error on purpose)Pop out in the code editor (opens in a new tab)TSX
import { createRoot } from "react-dom/client";import type { PropsWithChildren, ReactElement } from "react"; type NoteProps = PropsWithChildren<{ tone: "info" | "warning" }>; function Note({ tone, children }: NoteProps) {//                    ^?  return <p className={tone}>{children}</p>;} function Frame({ children }: { children: ReactElement }) {  return <div className="frame">{children}</div>;} createRoot(document.getElementById("root")!).render(  <Frame>    Free delivery over $20  </Frame>);

Line 17 is now plain text. TypeScript marks the <Frame> tag on line 16 on purpose: 'Frame' components don't accept text as child elements means text is a string, and Frame asked for an element. React itself would draw the text fine. The type is a rule you chose, so choose ReactElement only when a component really needs an element. Read the table row by row: the type you write, then what it lets in.

Ways to type children
children typed asWhat it lets inUse it when
ReactNodeElements, text, numbers, arrays of them, and null, undefined, true, or false.Almost always: a panel, a card, a layout.
ReactElementOnly an element, like <Note />. Not text.The component needs an element, for example to wrap it.
stringOnly text.A label or a button that shows only words.
PropsWithChildren<P>Your props P, plus an optional children that is a ReactNode.You want optional children without writing them out.

Typing event handlers

React calls your handlers with its own event objects, not the browser's. Their types come from React's types too: ChangeEvent, MouseEvent, FormEvent, and more. Each takes the element in angle brackets, like ChangeEvent<HTMLInputElement> (Typing events covers the browser's own events). When you write the handler inline, you don't need to name any of them:

An inline handler gets its event type for freePop out in the code editor (opens in a new tab)TSX
import { createRoot } from "react-dom/client"; function SearchBox() {  return <input placeholder="Search teas" onChange={(event) => console.log(event.target.value)} />;//                                                   ^?} createRoot(document.getElementById("root")!).render(<SearchBox />);

Line 4 passes an arrow function to onChange. TypeScript knows an input's onChange calls it with a change event from an input, so the query on line 5 shows (parameter) event: React.ChangeEvent<HTMLInputElement>. In React's change event, event.target is the input itself, so event.target.value is a string with no checks needed. Type tea in the box and the console shows t, te, and tea.

Move the same handler into a function of its own, and TypeScript can't work out the type anymore:

A handler on its own, with no event type (an error on purpose)Pop out in the code editor (opens in a new tab)TSX
import { createRoot } from "react-dom/client"; function SearchBox() {  function handleChange(event) {    console.log(event.target.value);  }  return <input placeholder="Search teas" onChange={handleChange} />;} createRoot(document.getElementById("root")!).render(<SearchBox />);

TypeScript types a function where you write it, not where you use it. On line 4, nothing says what event is, so it marks it on purpose: Parameter 'event' implicitly has an 'any' type means event would be any, which turns checking off, and strict mode doesn't let that happen silently (any, unknown & never explains any). Pick a type for line 5 below:

Playground: give the handler its event type
A search box with its own handlerPop out in the code editor (opens in a new tab)TSX
import { createRoot } from "react-dom/client";import type { ChangeEvent } from "react"; function SearchBox() {  function handleChange(event) {    console.log(event.target.value);  }  return <input placeholder="Search teas" onChange={handleChange} />;} createRoot(document.getElementById("root")!).render(<SearchBox />);
event isnot typed
What happens
TypeScript1 error: line 5
The pageLogs what you type: types are removed before it runs
Try it yourself

On its own, the function doesn't know where it will be used, so TypeScript says Parameter 'event' implicitly has an 'any' type.

TypeScript checks the code for real. Each button changes the parameter on line 5 and the type imported on line 2. Reset goes back to no type. Press Edit & run and type in the box to see each change logged.

Only ChangeEvent<HTMLInputElement> fits both the code inside the handler and the input it's given to. To find the right type for any handler, write it inline first and point at event: the editor shows the type to copy. The table lists the common ones. Read each row as: this handler, on this element, gets this event.

Common handlers and their event types
The handlerIts event type
onClick on a <button>React.MouseEvent<HTMLButtonElement, MouseEvent>
onChange on an <input>React.ChangeEvent<HTMLInputElement>
onChange on a <select>React.ChangeEvent<HTMLSelectElement>
onSubmit on a <form>React.FormEvent<HTMLFormElement>
onKeyDown on an <input>React.KeyboardEvent<HTMLInputElement>
onBlur on an <input>React.FocusEvent<HTMLInputElement, Element>

The second part of MouseEvent<HTMLButtonElement, MouseEvent> is the browser's own event, which React keeps in event.nativeEvent. A form's submit handler is another one to know:

A sign-up form's submit handlerPop out in the code editor (opens in a new tab)TSX
import { createRoot } from "react-dom/client";import type { FormEvent } from "react"; function SignupForm() {  function handleSubmit(event: FormEvent<HTMLFormElement>) {    event.preventDefault();    const data = new FormData(event.currentTarget);    console.log(`Signed up ${data.get("email")}`);  }  return (    <form onSubmit={handleSubmit}>      <input name="email" defaultValue="asha@example.com" />      <button>Sign up</button>    </form>  );} createRoot(document.getElementById("root")!).render(<SignupForm />);

Line 5 types the event as a FormEvent from a form. Line 6 stops the browser from reloading the page, its default action for a submit. On line 7, event.currentTarget is typed HTMLFormElement, so new FormData accepts it (FormData reads a form's fields). Press Sign up and the console shows Signed up asha@example.com.

Components also pass functions to each other. A child gets a function as a prop and calls it when something happens. Type that prop with a function type:

A button that tells its parent what was addedPop out in the code editor (opens in a new tab)TSX
import { createRoot } from "react-dom/client"; type AddButtonProps = {  productId: string;  onAdd: (productId: string) => void;}; function AddButton({ productId, onAdd }: AddButtonProps) {  return <button onClick={() => onAdd(productId)}>Add to cart</button>;} function logAdded(productId: string) {  console.log(`Added ${productId}`);} createRoot(document.getElementById("root")!).render(<AddButton productId="tea" onAdd={logAdded} />);

Line 5 says onAdd is a function that takes a product id, a string, and gives back nothing (void). Line 9 calls it with the id when the button is clicked. Line 12 defines logAdded, which fits that type, and line 16 passes it as onAdd. Click Add to cart and the console shows Added tea. TypeScript checks both sides: line 9 must call onAdd with a string, and line 16 must pass a function that takes one.

Top: the function logAdded(productId: string), with an arrow labelled logs to a console window that shows Added tea. An arrow down, labelled passed as onAdd, leads to the AddButton component. An arrow from AddButton, labelled click, leads to the call onAdd("tea"), and an arrow from that call, labelled runs logAdded, curves back up to logAdded.logAdded(productId: string)logsconsoleAdded teapassed as onAdd<AddButton />clickonAdd("tea")runs logAdded
The page hands logAdded to AddButton as its onAdd prop. A click calls onAdd("tea"), which runs logAdded. The type (productId: string) => void checks both sides: what the parent passes and what the child calls it with.

Optional and default props

A ? after a prop's name makes it optional, as in any object type. Callers can leave it out, and inside the component it may be undefined. A default value in the parameter list fills it in:

A button with an optional size and disabled statePop out in the code editor (opens in a new tab)TSX
import { createRoot } from "react-dom/client"; type ShopButtonProps = {  label: string;  size?: "small" | "medium" | "large";  disabled?: boolean;}; function ShopButton({ label, size = "medium", disabled = false }: ShopButtonProps) {//                           ^?  return <button className={`button-${size}`} disabled={disabled}>{label}</button>;} createRoot(document.getElementById("root")!).render(  <p>    <ShopButton label="Buy" /> <ShopButton label="Sold out" size="small" disabled />  </p>);

Lines 5 and 6 make size and disabled optional. Line 9 gives them defaults with =, the same default values JavaScript allows when you take an object apart (Destructuring). The query on line 10 shows size: "small" | "medium" | "large", with no undefined: with a default, it always has a value inside the component. Line 16 makes two buttons. Buy leaves out both props, so it gets medium and is enabled. Sold out passes size="small" and disabled with no value, which JSX reads as disabled={true}.

Change how line 10 below gives size a value, and watch the query and line 12:

Playground: a default for size
A button with an optional sizePop out in the code editor (opens in a new tab)TSX
import { createRoot } from "react-dom/client"; type ShopButtonProps = {  label: string;  size?: "small" | "medium" | "large";}; const paddings = { small: 4, medium: 8, large: 12 }; function ShopButton({ label, size = "medium" }: ShopButtonProps) {//                           ^?  const padding = paddings[size];  return <button style={{ padding }}>{label}</button>;} createRoot(document.getElementById("root")!).render(<ShopButton label="Buy" />);
size is"small" | "medium" | "large"
What happens
TypeScriptNo errors
The pageBuy button padding: 8px
Try it yourself

With a default, size always has a value inside the component, so its type has no undefined. The Buy button gets 8px of padding.

TypeScript checks the code for real, and the padding is what the Buy button gets when it runs. Each button changes only line 10. Reset goes back to size = "medium".

Older React code sets defaults with Component.defaultProps = { … }. React 19 ignores defaultProps on function components, so use default values in the parameter list, as above. Class components still support it.

A typed product list

A real shop page puts all of this together: an object prop, an optional slot for extra content, a callback, and a list. Read it top to bottom:

A product list with an Add button on each cardPop out in the code editor (opens in a new tab)TSX
import { createRoot } from "react-dom/client";import type { ReactNode } from "react"; type Product = {  id: string;  name: string;  price: number;}; type ProductCardProps = {  product: Product;  badge?: ReactNode;  onAdd: (product: Product) => void;}; function ProductCard({ product, badge, onAdd }: ProductCardProps) {  return (    <li>      {product.name}: ${product.price.toFixed(2)} {badge}      <button onClick={() => onAdd(product)}>Add {product.name}</button>    </li>  );} const products: Product[] = [  { id: "tea", name: "Green tea", price: 3 },  { id: "cake", name: "Lemon cake", price: 4.5 },]; function ProductList() {  return (    <ul>      {products.map((product) => (        <ProductCard          key={product.id}          product={product}          badge={product.price > 4 ? <strong>Treat</strong> : null}          onAdd={(added) => console.log(`Added ${added.name}`)}        />      ))}    </ul>  );} createRoot(document.getElementById("root")!).render(<ProductList />);
  • Lines 4 to 8 describe a product. The same type is used by the data, the card, and the callback.
  • Lines 10 to 14 are the card's props: the product object, an optional badge that can be anything React draws, and onAdd, which gets the whole product.
  • Line 19 shows the name, the price, and the badge. Line 20 calls onAdd with the product on a click.
  • Lines 25 to 28 are the products, typed Product[], so a missing price there would be marked too.
  • Lines 33 to 39 make one card per product. Line 35's key is React's own prop for lists: every component accepts it without listing it in its props.
  • Line 37 passes a <strong> element or null as the badge. Line 38's added gets the type Product from the props type, so added.name is checked.

The page shows Green tea: $3.00 and Lemon cake: $4.50 Treat, each with an Add button. Click Add Lemon cake and the console shows Added Lemon cake.

Now sort some tags. Each card below is checked on its own, with these two components:

The components the cards useTSX
import type { ReactNode } from "react"; function Panel(props: { title: string; children: ReactNode }) {  return <section>{props.children}</section>;} function ShopButton(props: { label: string; size?: "small" | "large" }) {  return <button>{props.label}</button>;}
Error or fine?
  • <ShopButton label="Buy" />
  • <ShopButton />
  • <ShopButton label="Buy" size="huge" />
  • <ShopButton label={3} />
  • <Panel title="Cart"><p>Tea</p></Panel>
  • <Panel title="Cart">{{ items: 2 }}</Panel>
  • <Panel title="Cart">{null}</Panel>
  • <input onChange={(event) => event.target.value} />
Try it yourself
0 of 8 correct

Sort each tag by what TypeScript says about it, using the Panel and ShopButton components above.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Common mix-ups

A name you don't import can quietly mean something else. Without an import, MouseEvent is the browser's event type, not React's:

The browser's MouseEvent on a React handler (an error on purpose)Pop out in the code editor (opens in a new tab)TSX
import { createRoot } from "react-dom/client"; function BuyButton() {  function handleClick(event: MouseEvent) {    console.log(`Clicked at ${event.clientX}`);  }  return <button onClick={handleClick}>Buy</button>;} createRoot(document.getElementById("root")!).render(<BuyButton />);

Line 4 types event as the browser's MouseEvent, and line 7 hands the function to onClick, which passes React's. TypeScript marks line 7 on purpose: Type '(event: MouseEvent) => void' is not assignable to type 'MouseEventHandler<HTMLButtonElement>' means the function wants a different kind of event than onClick gives. Add import type { MouseEvent } from "react" and it fits. The same goes for JSX:

JSX.Element with no import (an error on purpose)Pop out in the code editor (opens in a new tab)TSX
import { createRoot } from "react-dom/client"; function Logo(): JSX.Element {  return <strong>Tea House</strong>;} createRoot(document.getElementById("root")!).render(<Logo />);

Cannot find namespace 'JSX' on line 3 means there is no JSX to use. Older React types made JSX available everywhere. React 19's types keep it inside React, so import it:

JSX.Element imported from ReactPop out in the code editor (opens in a new tab)TSX
import { createRoot } from "react-dom/client";import type { JSX } from "react"; function Logo(): JSX.Element {  return <strong>Tea House</strong>;} createRoot(document.getElementById("root")!).render(<Logo />);

Line 2 imports the type, and line 4 uses it with no error. The page shows Tea House.

  • "TypeScript checks my props while the page runs." No. The types are removed first. Props built from a server reply can still be wrong, so check that data when it arrives (Typing fetch & JSON).
  • "Every component needs FC." No. A plain function with a typed parameter gets the same checks at every tag.
  • "Every component accepts children." No. Only a component whose props type lists children, and FC doesn't add it for you.
  • "onClick gets the browser's MouseEvent." No. It gets React's MouseEvent, which you import from "react".
  • "A prop with a default value is required." No. The ? keeps it optional for callers. The default only means it's never undefined inside the component.
  • "key has to be in my props type." No. key is React's own prop. Every component accepts it, and your component never sees it in its props.

Practice exercises

Each exercise is a small React component. Press Edit & run on the starter code: the errors update as you type, and the page and console show what it does. Try first, then use the hints.

Exercise 1 · Warm-upFind the line TypeScript marks

An order summary with one wrong prop. Which line will TypeScript mark?

import { createRoot } from "react-dom/client";

type OrderSummaryProps = {
  orderId: string;
  items: number;
  paid: boolean;
};

function OrderSummary({ orderId, items, paid }: OrderSummaryProps) {
  return <p>Order {orderId}: {items} items, {paid ? "paid" : "not paid"}</p>;
}

createRoot(document.getElementById("root")!).render(
  <OrderSummary orderId="A-17" items="3" paid />
);

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upRead a click handler's event type

    What type does TypeScript give event on line 4? Then try moving the handler into a function of its own, with that type.

    import { createRoot } from "react-dom/client"; function LikeButton() {  return <button onClick={(event) => console.log(event.type)}>Like</button>;} createRoot(document.getElementById("root")!).render(<LikeButton />);

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeWrite a props type

      TypeScript marks both props on line 3, because they have no type. Give the component a props type. What alt text does the image get?

      import { createRoot } from "react-dom/client"; function UserAvatar({ name, imageUrl }) {  return <img src={imageUrl} alt={`Photo of ${name}`} />;} createRoot(document.getElementById("root")!).render(  <UserAvatar name="Asha" imageUrl="/asha.png" />);

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeType a change handler

        Give event its type. Then press Edit & run and type 3 in the box. What does the console show?

        import { createRoot } from "react-dom/client"; function QuantityBox() {  function handleChange(event) {    console.log(`Quantity: ${event.target.valueAsNumber}`);  }  return <input type="number" aria-label="Quantity" onChange={handleChange} />;} createRoot(document.getElementById("root")!).render(<QuantityBox />);

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeMake a card's props optional

          Line 21 uses <InfoCard /> with nothing at all. Make both props optional, and give the title the default Good to know. What heading does the second card show?

          import { createRoot } from "react-dom/client";import type { ReactNode } from "react"; type InfoCardProps = {  title: string;  children: ReactNode;}; function InfoCard({ title, children }: InfoCardProps) {  return (    <section>      <h2>{title}</h2>      {children}    </section>  );} createRoot(document.getElementById("root")!).render(  <div>    <InfoCard title="Delivery">Free over $20</InfoCard>    <InfoCard />  </div>);

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeAllow only one to five stars

            A rating widget can call onRate(6), which no shop wants. Change the type of onRate so only 1 to 5 are allowed, then fix what TypeScript marks. What does clicking 5 stars print?

            import { createRoot } from "react-dom/client"; type StarRatingProps = {  onRate: (stars: number) => void;}; function StarRating({ onRate }: StarRatingProps) {  return (    <p>      <button onClick={() => onRate(1)}>1 star</button>      <button onClick={() => onRate(5)}>5 stars</button>      <button onClick={() => onRate(6)}>6 stars</button>    </p>  );} createRoot(document.getElementById("root")!).render(  <StarRating onRate={(stars) => console.log(`You gave ${stars} stars`)} />);

            Answer, then press Check. Spacing and letter case don’t matter.

              Check your understanding

              The typing React components quiz · 7 questionsScore: first tries count
              1. Question 1 of 7Which line does TypeScript mark in this price tag, and why?

                Read the code, then predictPop out in the code editor (opens in a new tab)TSX
                import { createRoot } from "react-dom/client";
                
                type PriceTagProps = {
                  amount: number;
                  currency: string;
                };
                
                function PriceTag({ amount, currency }: PriceTagProps) {
                  return <span>{amount} {currency}</span>;
                }
                
                createRoot(document.getElementById("root")!).render(<PriceTag amount={5} />);

                Choose an answer to see the explanation.

              2. Question 2 of 7What type does event get in this submit handler?

                Read the code, then predictPop out in the code editor (opens in a new tab)TSX
                import { createRoot } from "react-dom/client";
                
                function Newsletter() {
                  return <form onSubmit={(event) => event.preventDefault()}><button>Join</button></form>;
                //                        ^?
                }
                
                createRoot(document.getElementById("root")!).render(<Newsletter />);

                Choose an answer to see the explanation.

              3. Question 3 of 7What does this greeting show on the page?

                Read the code, then predictPop out in the code editor (opens in a new tab)TSX
                import { createRoot } from "react-dom/client";
                
                type GreetingProps = { name?: string };
                
                function Greeting({ name = "friend" }: GreetingProps) {
                  return <p>Hello, {name}!</p>;
                }
                
                createRoot(document.getElementById("root")!).render(<Greeting />);

                Choose an answer to see the explanation.

              4. Question 4 of 7Is {user.name} allowed as this card's children?

                Read the code, then predictPop out in the code editor (opens in a new tab)TSX
                import { createRoot } from "react-dom/client";
                import type { ReactNode } from "react";
                
                function UserCard({ children }: { children: ReactNode }) {
                  return <div>{children}</div>;
                }
                
                const user = { name: "Asha", city: "Pune" };
                
                createRoot(document.getElementById("root")!).render(<UserCard>{user.name}</UserCard>);

                Choose an answer to see the explanation.

              5. Question 5 of 7A panel should take text, elements, or nothing between its tags. Which type fits its children?

                Choose an answer to see the explanation.

              6. Question 6 of 7Why does function handleChange(event) need a type when the same arrow function inline in onChange doesn't?

                Choose an answer to see the explanation.

              7. Question 7 of 7A server sends a product with its price as text, and your code passes it to CartItem. What does TypeScript do about it?

                Choose an answer to see the explanation.

              Key takeaways

              • A component takes one props object. Type it like a parameter, after the whole pattern.
              • TypeScript checks every tag against the props type: missing, misspelled, and wrong-typed props are marked.
              • children is a prop like any other. Type it ReactNode unless you need something narrower.
              • Inline handlers get their event type from the element. A handler written on its own needs one, like ChangeEvent<HTMLInputElement>.
              • React's event types come from "react", not from the browser's globals.
              • Optional props use ?. Default values in the parameter list remove undefined inside.
              • Types are gone when the page runs, so data from outside still needs checking.

              A props type is the contract between a component and every tag that uses it.
              TypeScript checks that contract at each tag, so components get the props they expect.

              Coming next: Typing state & hooks. Components also remember things: state, reducers, refs, and context all have types too.

              CompleteFrontend Clear concepts. Working examples.