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.
- 01Type props and childrenWrite a props type, take it apart in the parameter list, and type children as ReactNode.
- 02Type event handlersLet inline handlers get their event type, and give handlers of their own types like ChangeEvent<HTMLInputElement>.
- 03Make 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:
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:
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 whereCartItemPropssays a number goes". TypeScript checks every tag that usesCartItem, before the code runs.
Change what line 15 passes and watch what TypeScript and the page say:
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>);price="4"1 error: line 15Stops: TypeError: price.toFixed is not a functionQuotes 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.
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.
<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.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
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.
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:
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:
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:
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:
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.
| The question | A plain function | FC |
|---|---|---|
| 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:
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, andfalse, which draw nothing.
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:
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>);<p>Tea × 2</p>No errorsShows Your cart, then Tea × 2An element is a ReactNode, so the panel takes it. React draws it under the heading.
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:
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:
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.
| children typed as | What it lets in | Use it when |
|---|---|---|
ReactNode | Elements, text, numbers, arrays of them, and null, undefined, true, or false. | Almost always: a panel, a card, a layout. |
ReactElement | Only an element, like <Note />. Not text. | The component needs an element, for example to wrap it. |
string | Only 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:
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:
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:
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 />);not typed1 error: line 5Logs what you type: types are removed before it runsOn its own, the function doesn't know where it will be used, so TypeScript says Parameter 'event' implicitly has an 'any' type.
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.
| The handler | Its 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:
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:
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.
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:
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:
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" />);"small" | "medium" | "large"No errorsBuy button padding: 8pxWith a default, size always has a value inside the component, so its type has no undefined. The Buy button gets 8px of padding.
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:
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
productobject, an optionalbadgethat can be anything React draws, andonAdd, which gets the whole product. - Line 19 shows the name, the price, and the badge. Line 20 calls
onAddwith 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
keyis React's own prop for lists: every component accepts it without listing it in its props. - Line 37 passes a
<strong>element ornullas the badge. Line 38'saddedgets the typeProductfrom the props type, soadded.nameis 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:
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>;}<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} />
Sort each tag by what TypeScript says about it, using the Panel and ShopButton components above.
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:
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:
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:
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, andFCdoesn'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 neverundefinedinside the component. - "key has to be in my props type." No.
keyis 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.
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 />
);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 />);Line 14 passes items="3", text, where OrderSummaryProps says a number goes, so TypeScript reports Type 'string' is not assignable to type 'number' there. Braces, items={3}, pass the number.
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 />);import { createRoot } from "react-dom/client";import type { MouseEvent } from "react"; function LikeButton() { function handleClick(event: MouseEvent<HTMLButtonElement>) { console.log(event.type); } return <button onClick={handleClick}>Like</button>;} createRoot(document.getElementById("root")!).render(<LikeButton />);A button's onClick passes React.MouseEvent<HTMLButtonElement, MouseEvent>. The solution moves the handler into a function of its own and gives event that type, imported from React.
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" />);import { createRoot } from "react-dom/client"; type UserAvatarProps = { name: string; imageUrl: string;}; function UserAvatar({ name, imageUrl }: UserAvatarProps) { return <img src={imageUrl} alt={`Photo of ${name}`} />;} createRoot(document.getElementById("root")!).render( <UserAvatar name="Asha" imageUrl="/asha.png" />);UserAvatarProps lists both props as strings, and : UserAvatarProps after the pattern types them. The image's alt text becomes Photo of Asha.
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 />);import { createRoot } from "react-dom/client";import type { ChangeEvent } from "react"; function QuantityBox() { function handleChange(event: ChangeEvent<HTMLInputElement>) { console.log(`Quantity: ${event.target.valueAsNumber}`); } return <input type="number" aria-label="Quantity" onChange={handleChange} />;} createRoot(document.getElementById("root")!).render(<QuantityBox />);ChangeEvent<HTMLInputElement> makes event.target the input, so valueAsNumber is a number. Typing 3 logs Quantity: 3.
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>);import { createRoot } from "react-dom/client";import type { ReactNode } from "react"; type InfoCardProps = { title?: string; children?: ReactNode;}; function InfoCard({ title = "Good to know", children }: InfoCardProps) { return ( <section> <h2>{title}</h2> {children} </section> );} createRoot(document.getElementById("root")!).render( <div> <InfoCard title="Delivery">Free over $20</InfoCard> <InfoCard /> </div>);With title? and children?, <InfoCard /> is allowed. The default title = "Good to know" gives the empty card a heading.
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`)} />);import { createRoot } from "react-dom/client"; type Stars = 1 | 2 | 3 | 4 | 5; type StarRatingProps = { onRate: (stars: Stars) => void;}; function StarRating({ onRate }: StarRatingProps) { return ( <p> <button onClick={() => onRate(1)}>1 star</button> <button onClick={() => onRate(5)}>5 stars</button> </p> );} createRoot(document.getElementById("root")!).render( <StarRating onRate={(stars) => console.log(`You gave ${stars} stars`)} />);onRate: (stars: Stars) => void lets the component call it only with 1 to 5, so onRate(6) became an error and the button went. The parent's stars is typed Stars too. Clicking 5 stars logs You gave 5 stars.
Check your understanding
Question 1 of 7Which line does TypeScript mark in this price tag, and why?
Read the code, then predictimport { 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.
Question 2 of 7What type does
eventget in this submit handler?Read the code, then predictimport { 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.
Question 3 of 7What does this greeting show on the page?
Read the code, then predictimport { 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.
Question 4 of 7Is
{user.name}allowed as this card's children?Read the code, then predictimport { 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.
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.
Question 6 of 7Why does
function handleChange(event)need a type when the same arrow function inline inonChangedoesn't?Choose an answer to see the explanation.
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.
childrenis a prop like any other. Type itReactNodeunless 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 removeundefinedinside. - 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.