Topic 14.1
Typing Components and Props
In one line
A component is a function that receives one object of props. Give that object a type and the compiler checks every place the component is used: missing props, wrong values and misspelt names become red errors.
Think of it like this
A café order slip with printed boxes: "Drink", "Size", "Notes (optional)". The barista can't make your coffee if the Drink box is empty, and a slip that says Size: "gigantic" gets sent back. A props type is the printed slip. The component is the barista: it only accepts slips that are filled in the way the form says.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Component
- A function that receives props and returns what React should draw.
- Props
- The one object a component receives from the place that uses it, like
{ title: "Hi" }. - Props type
- A
typethat lists which props exist, which are required and what type each has. - Optional prop
- A prop written with
?. It may be left out, so inside the component it may beundefined. - Default value
- A fallback written while destructuring (
tone = "plain") that is used when the prop is missing. - children
- Whatever is written between a component's opening and closing tags.
- ReactNode
- React's type for anything that can be drawn: text, numbers, elements, arrays of them,
null. - React.FC
- An older helper type for function components. Optional today; plain functions work as well.
- ComponentProps
- A helper that gives the props of an element (
"button") or of a component, so you can build on them.
Step by step
01Write the slip: a props type
Start with the shape of the props. title is required text, tone is optional and can be one of two words, children is anything React can draw.
Then put the type after the destructured parameter. Nothing else about the function changes.
type CardProps = {
title: string;
tone?: "plain" | "warning";
children: ReactNode;
};
function Card({ title, tone = "plain", children }: CardProps) {
return (
<section className={"card " + tone}>
<h2>{title}</h2>
{children}
</section>
);
}02The compiler checks every use
<Card title="Order" tone="warning">...</Card> is accepted. Leave out title and you get Property 'title' is missing in type ... but required in type 'CardProps'. Pass tone="loud" and you get Type '"loud"' is not assignable to type '"plain" | "warning"'.
The error appears on the tag that is wrong, not inside Card. That is what you want: the bug is where the call is.
03Optional and default
tone?: means callers may skip it. Writing tone = "plain" in the parameter list fills in the gap, so in the body tone is exactly "plain" | "warning", with no undefined to handle.
If a prop has no sensible default, leave it undefined and handle that case in the body.
04children and ReactNode
children: ReactNode accepts text, a number, one element, many elements or null. The Card above can be used with a paragraph, with plain text, or with a list.
children?: ReactNode (with the ?) is for components that work with nothing inside, like an icon button.
05Build on a native element with ComponentProps
To make your own Button, start from every attribute a real <button> accepts and add your own. Take your extras out, spread the rest onto the element.
Callers can now write <Button disabled onClick={...}> and TypeScript knows onClick's event is a mouse event on a button.
type ButtonProps = ComponentProps<"button"> & {
variant?: "primary" | "ghost";
};
function Button({ variant = "primary", className, ...rest }: ButtonProps) {
return <button className={"btn " + variant + (className ? " " + className : "")} {...rest} />;
}06You can skip React.FC
Both forms below accept the same props. The plain function is shorter, can be generic and needs no extra import.
// older style, still valid
const Hello: FC<{ name: string }> = ({ name }) => <p>Hello, {name}</p>;
// preferred today
function Hello2({ name }: { name: string }) {
return <p>Hello, {name}</p>;
}Try it yourself
- 1
Predict the error
In the first example, change
tone="warning"totone="danger". Predict the message before you look: which two words are allowed?Then add
tone?: "plain" | "warning" | "danger";toCardPropsand watch the error disappear. The type is the single place that decides what is allowed. - 2
Make a prop required
In the first example, remove the
?fromtoneinCardProps. Nothing breaks yet, because the tag inApppassestone="warning". Now also deletetone="warning"from the tag and read theTS2741error.Notice the default
tone = "plain"still compiles: a default on a required prop is allowed but pointless. - 3
Pass something the button doesn't know
In the third example add
href="/home"to<Button>. A<button>has nohref, soComponentProps<"button">rejects it (Property 'href' does not exist). Now make the component an anchor: changeComponentProps<"button">toComponentProps<"a">, rename the<buttonelement insideButtonto<a, and changedocument.querySelector("button")to"a". Thehrefis accepted and the// ^?now saysMouseEvent<HTMLAnchorElement, MouseEvent>.
Code & diagrams
The type-check is clean, then React draws the card and the last line prints the HTML it made. Try removing title="Order #42" from the tag and read the error.
Preview
Console
Nothing logged yet.Expected console output
<section class="card warning"><h2>Order #42</h2><p>Two items, waiting for payment.</p></section>Each error sits on the tag that is wrong. The page still draws (like tsc, the preview runs code that has type errors), so the Console shows the bugs the errors predicted: an empty heading and a CSS class called loud.
Preview
Console
Nothing logged yet.Expected console output
<section class="card warning"><h2></h2>No title given</section><section class="card loud"><h2>Loud</h2>Unknown tone</section><section class="card plain"><h2>Empty</h2></section>You never wrote a type for event: it comes from ComponentProps<"button">. The script clicks the button after 100 ms so the Console shows the handler running.
Preview
Console
Nothing logged yet.Expected console output
clicked BUTTON
<button class="btn ghost" type="button">Save</button>Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Forget a required prop
In the first example, delete title="Order #42" from the <Card> tag so it reads <Card tone="warning">.
Break #2
A typo in an attribute value
In the third example, change type="button" to type="buton".
Myth vs fact
Myth
Components must be typed with React.FC.
Fact
They never had to be. A plain function with a typed parameter is the common style now, and unlike FC it can be generic.
Myth
children should be typed as JSX.Element.
Fact
That rejects plain text, numbers, null and arrays. ReactNode accepts everything React can draw.
Myth
A props type checks the data at runtime.
Fact
It exists only while compiling. If props come from a server, validate them at the boundary (Topic 13.4, Topic 14.6).
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Props are compared structurally (Topic 3.5), so an object with extra properties assigned to a variable first can be spread into a component without an error; only a fresh object literal in JSX gets the excess-property check. Rely on the tag syntax for the strict check.
- ▸
ComponentProps<T>is a conditional type (Topic 9.1): for a string like"button"it looks upJSX.IntrinsicElements, for a component it infers its props withinfer.ComponentPropsWithoutRef<T>removesrefand is the safer base for wrappers on older React; in React 19refis an ordinary prop on function components (Topic 14.6). - ▸
Prefer
typealiases for props when you need unions or intersections (ButtonPropsabove); aninterfaceis fine for a plain object shape and gives slightly shorter error messages (Topic 3.4). Both are checked the same way. - ▸
Default values in the parameter list are inference-friendly: TypeScript removes
undefinedfrom the prop's type inside the body. That is not true for a default set later with??in a variable, which you handle by hand.
Remember this
- 1
The problem this solves: in plain JavaScript you can use a component with a missing or misspelt prop and nothing complains until the page shows
undefinedor breaks. A props type lets the compiler check every use before the code runs. In React, a component is a function that returns what to draw, and props (short for properties) are the one object it receives:<Card title="Hi" />callsCard({ title: "Hi" }). In plain JavaScript nothing says which props exist. In TypeScript you write a type for that object, usually atype CardProps = { ... }, and put it after the destructured parameter:function Card({ title }: CardProps). This is the props type. - 2
The compiler now checks every use of
<Card ... />. A required prop (no?) that is missing is an error (TS2741). A prop with the wrong type, such astone="loud"where only"plain" | "warning"is allowed, is an error (TS2322). A prop that doesn't exist is an error too. This is how you find a renamed prop in a hundred files in seconds: change the type, read the red list. - 3
Optional props use
?:tone?: "plain" | "warning". Inside the component, an optional prop may beundefined, so you either handle that or give it a default value while destructuring:{ tone = "plain" }. After the default, TypeScript knowstoneis neverundefinedinside the function, but callers still don't have to pass it. Literal unions like"plain" | "warning"(Topic 4.2) are the best way to type a prop that has a few allowed words, far better thanstring. - 4
**
children** is a prop like any other: what you write between the opening and closing tag. Its type isReactNode, which React defines as everything it can draw: strings, numbers, elements, arrays of those,null,undefinedand booleans. Writechildren: ReactNodeto require content andchildren?: ReactNodeto allow none.ReactNodeis the right default;ReactElementis narrower (only elements, no plain text) and breaks<Card>just text</Card>. - 5
**
React.FCis optional.** Older code writesconst Card: FC<CardProps> = ({ title }) => .... Since @types/react 18,FCno longer adds an invisiblechildrenprop, so it gives you very little: it can't be generic (Topic 14.5), it makes the function's return type less precise, and a plainfunction Card(props: CardProps)reads the same and infers its return type. Most teams today write plain functions. You'll still seeFCin old code and it is fine to read. - 6
To build a component on top of a native element, such as a button that adds a
variantprop but still acceptsonClick,disabledandtype, don't retype every attribute. Use **ComponentProps<"button">**: it is the full set of props a<button>takes, taken straight from React's types. Intersect it with your own extras (ComponentProps<"button"> & { variant?: ... }), pull out the extras you use, and spread the rest onto the element. Handlers on such props are typed for you: theeventinonClickis already aMouseEvent<HTMLButtonElement>. - 7
Two traps. A props type describes what callers may pass; it doesn't validate at runtime, so data from a server still needs checking (Topic 13.4). And never type props as
anyorobject"for now": every use then goes unchecked, which is the opposite of why you are here.
Explain it without notes
How do you type the props of a React component, and what does the compiler check?
What is the difference between ReactNode, ReactElement and JSX.Element, and which do you use for children?
Why do many teams no longer use React.FC?
How do you build a wrapper component around a native element and keep all its native props?
Practice
Write a component Avatar with props src: string, alt: string and optional size?: "sm" | "lg" (default "sm"). Then write a use that fails because size="xl". What does tsc say?
Write IconButton that accepts everything a <button> accepts plus a required icon: string. Show that <IconButton icon="x" disabled /> is fine and <IconButton disabled /> is an error.
Trade-offs
- ↔
Reusing
ComponentProps<"button">gives you every native attribute for free, but it also lets callers pass props you never meant to support (formAction,popoverTarget). For a design-system component, expose a smaller, hand-written props type instead. - ↔
Strict unions for props (
"plain" | "warning") catch typos but cost a type change whenever a designer adds a variant. That is usually the point: every place that needs updating is listed for you.
Done when you can
I can write a props type and annotate a component with it.
I can make a prop optional and give it a default, and say what its type is inside the function.
I can type
childrenasReactNodeand explain why notJSX.Element.I can explain why
React.FCis optional.I can build a wrapper with
ComponentProps<"button">and spread the rest.I can read
TS2741andTS2322on a JSX tag.