Command Palette

Search for a command to run...

Hectal
PHASE 14Intermediate ~22 min· topic 1 of 6

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 type that 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 be undefined.
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.

Card.tsxwhole filetsx
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.

Button.tsxwhole filetsx
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.

fc.tsxwhole filetsx
// 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. 1

    Predict the error

    In the first example, change tone="warning" to tone="danger". Predict the message before you look: which two words are allowed?

    Then add tone?: "plain" | "warning" | "danger"; to CardProps and watch the error disappear. The type is the single place that decides what is allowed.

  2. 2

    Make a prop required

    In the first example, remove the ? from tone in CardProps. Nothing breaks yet, because the tag in App passes tone="warning". Now also delete tone="warning" from the tag and read the TS2741 error.

    Notice the default tone = "plain" still compiles: a default on a required prop is allowed but pointless.

  3. 3

    Pass something the button doesn't know

    In the third example add href="/home" to <Button>. A <button> has no href, so ComponentProps<"button"> rejects it (Property 'href' does not exist). Now make the component an anchor: change ComponentProps<"button"> to ComponentProps<"a">, rename the <button element inside Button to <a, and change document.querySelector("button") to "a". The href is accepted and the // ^? now says MouseEvent<HTMLAnchorElement, MouseEvent>.

Code & diagrams

A typed Card: required, optional with a default, and children

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

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

<section class="card warning"><h2>Order #42</h2><p>Two items, waiting for payment.</p></section>
Three wrong uses: missing prop, wrong value, missing children

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

Press Run to see the page.

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>
A Button built on ComponentProps<"button">

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

Press Run to see the page.

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">.

terminal
$ Types panel
── what you'll see ──
index.tsx(21,6): error TS2741: Property 'title' is missing in type '{ tone: "warning"; children: Element; }' but required in type 'CardProps'.

Break #2

A typo in an attribute value

In the third example, change type="button" to type="buton".

terminal
$ Types panel
── what you'll see ──
index.tsx(15,7): error TS2820: Type '"buton"' is not assignable to type '"button" | "reset" | "submit" | undefined'. Did you mean '"button"'?

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 up JSX.IntrinsicElements, for a component it infers its props with infer. ComponentPropsWithoutRef<T> removes ref and is the safer base for wrappers on older React; in React 19 ref is an ordinary prop on function components (Topic 14.6).

  • ▸

    Prefer type aliases for props when you need unions or intersections (ButtonProps above); an interface is 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 undefined from 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. 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 undefined or 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" /> calls Card({ title: "Hi" }). In plain JavaScript nothing says which props exist. In TypeScript you write a type for that object, usually a type CardProps = { ... }, and put it after the destructured parameter: function Card({ title }: CardProps). This is the props type.

  2. 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 as tone="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. 3

    Optional props use ?: tone?: "plain" | "warning". Inside the component, an optional prop may be undefined, so you either handle that or give it a default value while destructuring: { tone = "plain" }. After the default, TypeScript knows tone is never undefined inside 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 than string.

  4. 4

    **children** is a prop like any other: what you write between the opening and closing tag. Its type is ReactNode, which React defines as everything it can draw: strings, numbers, elements, arrays of those, null, undefined and booleans. Write children: ReactNode to require content and children?: ReactNode to allow none. ReactNode is the right default; ReactElement is narrower (only elements, no plain text) and breaks <Card>just text</Card>.

  5. 5

    **React.FC is optional.** Older code writes const Card: FC<CardProps> = ({ title }) => .... Since @types/react 18, FC no longer adds an invisible children prop, 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 plain function Card(props: CardProps) reads the same and infers its return type. Most teams today write plain functions. You'll still see FC in old code and it is fine to read.

  6. 6

    To build a component on top of a native element, such as a button that adds a variant prop but still accepts onClick, disabled and type, 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: the event in onClick is already a MouseEvent<HTMLButtonElement>.

  7. 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 any or object "for now": every use then goes unchecked, which is the opposite of why you are here.

Explain it without notes

01

How do you type the props of a React component, and what does the compiler check?

02

What is the difference between ReactNode, ReactElement and JSX.Element, and which do you use for children?

03

Why do many teams no longer use React.FC?

04

How do you build a wrapper component around a native element and keep all its native props?

Practice

01

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?

02

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 children as ReactNode and explain why not JSX.Element.

  • I can explain why React.FC is optional.

  • I can build a wrapper with ComponentProps<"button"> and spread the rest.

  • I can read TS2741 and TS2322 on a JSX tag.