TypeScript
Completed
-
Section 1: Getting Started
-
Section 2: Basic Types
-
Section 3: Functions and Objects
-
Section 4: Advanced Types
-
Section 5: Object-Oriented TypeScript
-
Section 6: Working with Modules
-
Section 7: Tooling and Practice
-
Section 8: Type-Level Programming
-
Section 9: TypeScript with Backends
-
Section 10: Testing Typed Code
-
Section 11: Data Validation with Types
-
Section 12: Practical Projects
-
Section 13: Compiler Internals
-
Section 14: Configuration Deep Dive
-
Section 15: Enums, Symbols, and Special Types
-
Section 16: Working with Async Code
-
Section 17: TypeScript and the DOM
-
Section 18: Advanced Generics Practice
-
Section 19: Working with Third-Party Types
-
Section 20: Monorepo and Large-Scale Practices
-
Section 21: Common Pitfalls and Best Practices
-
Section 22: Interview Practice
-
Section 23: Handbook: Narrowing In Depth
-
Section 24: Handbook: Object Types In Depth
-
Section 25: Handbook: Classes In Depth
-
Section 26: Handbook: Modules In Depth
-
Section 27: Handbook: Declaration Files In Depth
-
Section 28: JSX and Namespaces
-
Section 29: Compiler Configuration Reference
-
Section 30: More Practice Exercises
-
Section 31: Handbook: Everyday Types Deep Dive
-
Section 32: Utility Types Full Reference
-
Section 33: Decorators Reference
-
Section 34: Mixins and Advanced OOP Patterns
-
Section 35: Iterators and Generators Typing
-
Section 36: More Type-Level Programming Practice
-
Section 37: TypeScript Ecosystem Tools
-
Section 38: TypeScript with Testing Frameworks
-
Section 39: TypeScript for Library Authors
-
Section 40: More Interview Practice
-
Section 41: Handbook: Functions In Depth
-
Section 42: Handbook: Type Manipulation Deep Dive
-
Section 43: More Real-World Patterns
-
Section 44: TypeScript Release Notes Highlights
-
Section 45: Final Practice and Review
165: Typing JSX Elements
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
PageLayoutcomponent. - Define a
PageLayoutPropsinterface wheresidebarandmainContentare 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.
There are no comments for now.