Fixing JSX types between Preact and React Libraries

  • infoFull Post Details
    info_outlineClick for Full Post Details
    Date Posted:
    Sep. 06, 2020
    Last Updated:
    Sep. 06, 2020
  • classTags
    classClick for Tags

The Issue

I have a small project that I’m starting, and I decided to try out Preact for the first time, using their TypeScript starter template. Everything installed (relatively) smoothly, and it seemed like everything was fine… until I tried to add a third-party component library. Then everything exploded.

To be specific, I tried to add Chakra UI, and immediately got this error (“cannot be used as a JSX component”) when trying to add any component:

'___' cannot be used as a JSX component.
  Its return type 'ReactElement<any, any> | null' is not a valid JSX element.
    Type 'ReactElement<any, any>' is not assignable to type 'Element'.ts(2786)

Trying to chase down this incompatible return type (“not a valid JSX element”) led me down a long trail, but the basics seemed to be that Preact components, such as FunctionalComponent (used by the template’s default App.tsx file) expect preact.VNode elements to make up children / returned elements / etc. This makes sense; preact.VNode is part of Preact’s VDOM implementation, which is distinctly different from React’s. It’s also worth noting where this comes from; in Preact’s JSX system, Element (aka JSX.Element) extends preact.VNode<any>.

What confused me about this error was two things:

  1. Why is this happening? According to this page, preact-compat is included by default in the newer versions of Preact. And I’ve followed the one step listed in the docs – setting skipLibCheck to true.
  2. How do I fix this? Is there a way in TypeScript to override namespaces and module definitions, to the extent that it could fix third party imported code?

The Solutions

The first issue I found (react-colorful/#39) was very close to mine, but not yet solved. However, after some more digging, I finally ended up on preact #2222, #2150, and probably the most relevant, PR #2329.

🚧 Once PR #2329 is merged and released, I believe that this issue will no longer exist, and you won’t have to do any manual patching to get third party React libraries to work.

To summarize, the main issue seems to be that, although Preact is “shipping” a lot of types needed for React compatibility, they aren’t necessarily getting automatically “pulled in” for TypeScript to understand that a third-party lib’s use of JSX Element should be aliased / pointed to Preact’s implementation.

Option A – Use Path Mapping

TypeScript’s config has an incredibly powerful option that allows import paths to be remapped to destinations of our choice; the paths option.

For our case, we can use this to remap the resolution of react and react-dom to the preact-compat layer, which exposes Preact’s JSX implementation, among other Preact types.

Here is the change to tsconfig.json (thanks to this comment):

{
    "compilerOptions": {
        // Needed in most cases
        "skipLibCheck": true,

        // You need a value set for baseUrl
        "baseUrl": ".",

        // This is the key - module resolution path remapping
        "paths": {
            "react": ["node_modules/preact/compat/"],
            "react-dom": ["node_modules/preact/compat/"]
        }
    }
}

⚠ Warning: If you are using a separate bundler / compiler (outside of TSC), you might also need to inform it of the path aliasing. For example, if you use Parcel.

If the types exported from preact/compat don’t cover it for you, and you need to provide more React type shims, another variant of the above solution is to create your own compatibility declaration file (react/index.d.ts), and use the paths option to map to it. This approach is used in the issue #2150 thread.

Option B – Manually Override Types

I couldn’t get this working 100% (at least not without turning on skipLibCheck), but I believe it might be possible to “monkey-patch” the definitions for the React module in-place, by using TypeScript’s module augmentation and declaration merging features.

This was my (bad) attempt, which was close to working, but violated a lot of TS rules:

import * as React from 'react';

declare module 'react' {
    namespace React {
        type FC<P = {}> = preact.FunctionComponent<P>;
        // ...
    }
    export = React;
    export as namespace React;
}

However, I have a feeling this is the wrong approach; namespace merging is a complicated topic with TypeScript, and it looks like the best solution for global module augmentation is to follow the pattern of:

  1. Creating a folder with the module name
  2. Creating an index file in that folder (/{moduleName}/index.d.ts)
  3. Telling TSC about the file (if necessary)

Again, these steps are outlined in this comment on #2150, and partially in the TS Docs. If you can find better documentation / best practices, please let me know; this is a part of TS that I always feel I could use a better grasp of.

2 thoughts on “Fixing JSX types between Preact and React Libraries”

  1. Momchil says:

    Thank you so much for solving my issue. This crap bugged me for the past hour or so.

    1. joshuatz says:

      Glad to have helped!

Leave a Reply

Your email address will not be published. Required fields are marked *