VOOZH about

URL: https://blog.logrocket.com/declaring-jsx-types-typescript-5-1/

⇱ Declaring JSX types in TypeScript 5.1 - LogRocket Blog


2023-06-26
1061
#typescript
John Reilly
172664
107
👁 Image

See how LogRocket's Galileo AI surfaces the most severe issues for you

No signup required

Check it out

A new feature, described as “decoupled type-checking between JSX elements and JSX tag types”, arrives with TypeScript 5.1. This feature enables libraries to control what types are used for JSX elements. In this article, I’ll discuss why this matters and how this new feature works.

👁 Declaring JSX Types TypeScript

Jump ahead:

🚀 Sign up for The Replay newsletter

The Replay is a weekly newsletter for dev and engineering leaders.

Delivered once a week, it's your curated guide to the most important conversations around frontend dev, emerging AI tools, and the state of modern software.

Working with JSX in TypeScript

Until version 5.1, TypeScript did an imperfect job of representing what is possible with JSX. The JSX decoupled type-checking feature allows libraries to do a better job of that, by handing them control of JSX type definitions.

It’s probably worth noting that JSX decoupled type-checking is a complicated feature. If you don’t understand it, that’s okay! I’ll confess that as the author of this post, I had to work quite hard to fully comprehend it.

This is a low-level feature that is only likely to be used by library and type definition authors. It’s a primitive that will unlock possibilities for those who are writing JSX without requiring any extra action on their part. In some cases, people writing JSX may not even notice that things have changed for the better.

Understanding the problem

TypeScript creates a type system that sits on top of JavaScript and provides static typing capabilities. As TypeScript has grown more sophisticated, it’s been able to get closer and closer to representing the full range of possibilities that JavaScript offers.

An example of this evolution was the introduction of union types. If you remember the early days of TypeScript, you’ll recall a time before union types. Back then, we had to use any to represent a value that could be one of a number of types. Union types solved this imperfect representation of JavaScript:

-function printStringOrNumber(stringOrNumber: any) {
+function printStringOrNumber(stringOrNumber: string | number) {
 console.log(stringOrNumber);
}

The problem we’re looking at in this article is in the same vein, but it specifically applies to JSX — which is widely used in libraries, like React. Prior to v5.1, TypeScript lacked the ability to accurately represent all JSX possibilities. This is because the type of JSX element returned from a function component was always JSX.Element | null. This is a type that is defined in the TypeScript compiler; it cannot be changed by a library author.

Let’s take a look at a simple example to see how this plays out. Say we have a function component that returns a number. We might write something like this:

function ComponentThatReturnsANumber() {
 return 42;
}

<ComponentThatReturnsANumber />;

The above code is legitimate JSX, but it is not legitimate TypeScript. As a result, the TypeScript compiler will complain:

👁 TypeScript Error Message Return Type Number Invalid JSX Element

You can view this in the TypeScript Playground. The error is thrown because, according to TypeScript, function components that return anything except JSX.Element | null are not allowed as element types in React.

However, in React, function components can return a ReactNode. This type includes number | string | Iterable<ReactNode> | undefined and will likely also include Promise<ReactNode>( in the future.

As an aside, a return value of number would be perfectly fine in class components since the restrictions are different there. I spoke to Sebastian Silbermann, who wrote the PR requesting the new feature, about this and he said:

“An interesting note is that before function components we did have full control. Due to ElementClass, class components already could return ReactNode at the type level. It was just function components that were missing full control (or any other component types Suspense or Profiler).”

So here’s the crux of the problem: it is not possible to represent in TypeScript today what is actually possible in React (or in other JSX libraries). Furthermore, what’s returned from JSX may change over time, and TypeScript needs to be able to represent that.

The arrival of JSX.ElementType

In an effort to address the issue described in the previous section, Sebastian opened a pull request to TypeScript: “RFC: Consult new JSX.ElementType for valid JSX element types“. In that PR, Sebastian explained the issue and proposed a solution — introducing a new type, JSX.ElementType.

Here’s an illustration that helps explain what the JSX.ElementType is compared to a JSX element:

// <Component />
// ^^^^^^^^^ JSX element type
// ^^^^^^^^^^^^^ JSX element

The significance of JSX.ElementType is that it is used to represent a JSX element’s type and to allow library authors to control what types are used for JSX elements. This control was not previously available.

The TypeScript pull request was merged, so Sebastian (who helps maintain the React type definitions) exercised new powers in this pull request to the DefinitelyTyped repository for the React type definitions. At the time of writing, this pull request is still open, but once merged and shipped the React community we will feel its benefits.

The changes associated with this new feature are subtle; you can see in this pull request that ReactElement | null is generally replaced with ReactNode:

 type JSXElementConstructor<P> =
- | ((props: P) => ReactElement<any, any> | null)
+ | ((props: P) => ReactNode)
 | (new (props: P) => Component<any, any>);

Remember how we mentioned earlier that function components couldn’t return numbers? Let’s look at the updated tests in the PR:

 const ReturnNumber = () => 0xeac1;
+ const FCNumber: React.FC = ReturnNumber;
 class RenderNumber extends React.Component {
 render() {
 return 0xeac1;
 }
 }

With this change, React components that return numbers are now valid JSX elements. This is because JSX.ElementType is now ReactNode, which includes numbers. New things are possible as a consequence of this change. The library and type definition author now has more control over what is possible in JSX.

To quote Sebastian again, “Now we have control over any potential component type.”

Let’s take another look at our component that produces a number:

function ComponentThatReturnsANumber() {
 return 42;
}

<ComponentThatReturnsANumber />;

With Sebastian’s changes, this becomes valid TypeScript. And as React and other JSX libraries evolve, TypeScript compatibility will evolve as well.

Summary

The TL;DR of this post is that TypeScript will better allow for the modeling of JSX in TypeScript 5.1. I’m indebted to Sebastian Silbermann and Daniel Rosenwasser for their explanations of the decoupled type-checking between JSX elements and JSX tag types feature.

A special thanks to Sebastian for implementing this feature and for reviewing this article. I hope this post helps improve your understanding of this new TypeScript feature.


Over 200k developers use LogRocket to create better digital experiences

👁 Image
Learn more →

LogRocket understands everything users do in your web and mobile apps.

👁 LogRocket Dashboard Free Trial Banner

LogRocket lets you replay user sessions, eliminating guesswork by showing exactly what users experienced. It captures console logs, errors, network requests, and pixel-perfect DOM recordings — compatible with all frameworks, and with plugins to log additional context from Redux, Vuex, and @ngrx/store.

With Galileo AI, you can instantly identify and explain user struggles with automated monitoring of your entire product experience.

Modernize how you understand your web and mobile apps — start monitoring for free.

👁 Image
👁 Image
👁 Image

Stop guessing about your digital experience with LogRocket

Get started for free

Recent posts:

What is TSRX?: What JSX would look like if it were designed today

TSRX adds first-class control flow, conditional hooks, and scoped styles to React via a TypeScript compiler extension — no new framework required.

👁 Image
Ikeh Akinyemi
Jun 12, 2026 ⋅ 6 min read

How to add authentication to a React Native app with Better Auth

Learn how to build a full React Native auth system using Better Auth and Expo — with email/password login, Google OAuth, session persistence, and protected routes.

👁 Image
Chinwike Maduabuchi
Jun 9, 2026 ⋅ 13 min read

AI dev tool power rankings & comparison [June 2026]

Compare the top AI development tools and models of June 2026. View updated rankings, feature breakdowns, and find the best fit for you.

👁 Image
Chizaram Ken
Jun 8, 2026 ⋅ 11 min read

How to check username availability at scale with Bloom filters

Learn how Bloom filters reduce database lookups for username availability checks while preserving correctness at scale.

👁 Image
Rosario De Chiara
Jun 8, 2026 ⋅ 6 min read
View all posts

Would you be interested in joining LogRocket's developer community?

Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.

Sign up now