DEV Community

Cover image for Declaring JSX types in TypeScript 5.1
Matt Angelosanto for LogRocket

Posted on • Originally published at blog.logrocket.com

Declaring JSX types in TypeScript 5.1

Written by John Reilly✏️

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.

Jump ahead:

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);
}
Enter fullscreen mode Exit fullscreen mode

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 />;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>);
Enter fullscreen mode Exit fullscreen mode

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;
        }
    }
Enter fullscreen mode Exit fullscreen mode

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 />;
Enter fullscreen mode Exit fullscreen mode

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.


Get setup with LogRocket's modern TypeScript error tracking in minutes:

1.Visit https://logrocket.com/signup/ to get an app ID.
2.Install LogRocket via NPM or script tag. LogRocket.init() must be called client-side, not server-side.

NPM:

$ npm i --save logrocket 

// Code:

import LogRocket from 'logrocket'; 
LogRocket.init('app/id');
Enter fullscreen mode Exit fullscreen mode

Script Tag:

Add to your HTML:

<script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script>
<script>window.LogRocket && window.LogRocket.init('app/id');</script>
Enter fullscreen mode Exit fullscreen mode

3.(Optional) Install plugins for deeper integrations with your stack:

  • Redux middleware
  • ngrx middleware
  • Vuex plugin

Get started now

Top comments (0)