Declaring JSX types in TypeScript 5.1
Tags: webdev, typescript
Published: 2023-07-06 14:24:54

**Written by [John Reilly](https://blog.logrocket.com/author/johnreilly/)✏️**

A new feature, described as “[decoupled type-checking between JSX elements and JSX tag types](https://devblogs.microsoft.com/typescript/announcing-typescript-5-1-beta/#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](#working-with-jsx-typescript)
*   [Understanding the problem](#understanding-the-problem)
*   [The arrival of `JSX.ElementType`](#arrival-jsx-elementtype)

## Working with JSX in TypeScript <a name="working-with-jsx-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](https://blog.logrocket.com/understanding-discriminated-union-intersection-types-typescript/). 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:

```typescript
-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:

```typescript
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](http://blog.logrocket.com/wp-content/uploads/2023/06/typescript-playground-error-message-return-type-number-invalid-jsx-element.png) 

You can view this in the [TypeScript Playground](https://www.typescriptlang.org/play?#code/JYWwDg9gTgLgBAJQKYEMDG8BmUIjgIilQ3wG4AoczAVwDsNgJa4BhXSWpWmAFQAsUMZDGpRaAZwCCAOWogARkigAKAJRwA3uThwiIsXAAsAJgoBfSgB424Jl14ChSfRJlzFUOAHoAfOSA). 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](https://github.com/reactjs/rfcs/pull/229) `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](https://twitter.com/sebsilbermann), 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` <a name="arrival-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](https://github.com/microsoft/TypeScript/pull/51328)". 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:

```javascript
// <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](https://github.com/DefinitelyTyped/DefinitelyTyped/pull/65135). 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`:

```typescript
     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:

```typescript
    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:

```typescript
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](https://github.com/eps1lon) and [Daniel Rosenwasser](https://github.com/DanielRosenwasser) 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:
```bash
$ npm i --save logrocket 

// Code:

import LogRocket from 'logrocket'; 
LogRocket.init('app/id');
```
Script Tag:
```javascript
Add to your HTML:

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

3.(Optional) Install plugins for deeper integrations with your stack:
*   Redux middleware
*   ngrx middleware
*   Vuex plugin

[Get started now](https://lp.logrocket.com/blg/signup)