Skip to Content
Course content

165: Typing JSX Elements

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I was working on a dashboard project yesterday and ran into a classic "TypeScript friction" moment. I wanted to create a generic Notification component that could take a custom icon or a piece of formatted text as a header. At first, I just let TypeScript infer everything, but as soon as I tried to move some logic into a helper function, the compiler started complaining.

Wait, what is this actually returning?

I started with a simple helper function. My goal was to take some content and wrap it in a stylized span for consistency across the app. Here is how I first wrote it:

function wrapInBadge(content: any) {
  return <span className="badge">{content}</span>;
}

Using any is a sin, obviously. I wanted to be explicit. I figured, "This returns JSX, so I'll just look up the type." I found JSX.Element, which sounded exactly right. It's literally a JSX element, right?

function wrapInBadge(content: JSX.Element): JSX.Element {
  return <span className="badge">{content}</span>;
}

This looked fine until I actually tried to use it. I tried calling wrapInBadge("Alert!"). TypeScript immediately threw a fit: Argument of type 'string' is not assignable to parameter of type 'JSX.Element'.

The narrow path of JSX.Element

This is where it gets tricky. I realized that JSX.Element is incredibly strict. It represents a single JSX element (like <div /> or <span />). But in the real world, "content" in a component isn't always a single element. Sometimes it's a string, sometimes it's a number, sometimes it's an array of elements, and sometimes it's just null because something is conditionally hidden.

If I want my wrapInBadge function to be useful, it needs to accept anything that React is capable of rendering. JSX.Element is too narrow for a prop, even if it's perfectly fine for a function return type.

Opening it up with ReactNode

I did a bit of digging into the React type definitions and found ReactNode. Unlike JSX.Element, ReactNode is essentially a union type. It includes ReactElement, string, number, Iterable<ReactNode>, ReactPortal, boolean, null, and undefined.

Let's see what happens when we swap those out:

import React, { ReactNode } from 'react';

function wrapInBadge(content: ReactNode): JSX.Element {
  return <span className="badge">{content}</span>;
}

// Now these all work:
wrapInBadge("Alert!"); // String - OK
wrapInBadge(<Strong>Critical!</Strong>); // JSX.Element - OK
wrapInBadge(404); // Number - OK
wrapInBadge(null); // Null - OK

Notice that I kept the return type as JSX.Element. Why? Because the function definitely returns a <span>. It's not returning a string or a number; it's returning a specific piece of JSX. This is a good rule of thumb: use ReactNode for things that go into your components (props, children) and JSX.Element (or ReactElement) for the thing your component spits out.

A quick word on ReactElement

You might see ReactElement floating around too. For most of your daily work, JSX.Element and ReactElement are nearly interchangeable. JSX.Element is actually just a shorthand for ReactElement<any, any, any>. I generally stick to JSX.Element for return types because it's more concise and describes exactly what the code looks like.




📋 Practical Task

The Flexible Layout Wrapper

You are building a PageLayout component. This component has a sidebar prop and a mainContent prop. Both of these should be flexible enough to accept a simple string, a fragment of JSX, or even null if the sidebar is disabled.

Your Task:

  • Create a PageLayout component.
  • Define a PageLayoutProps interface where sidebar and mainContent are typed correctly to allow any renderable React content.
  • Ensure the component itself returns a JSX.Element.
  • Implement the component so it renders a <div> containing two child <aside> and <main> tags that output the props.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.