TypeScript: Unable to `import` JSX types, Error: "no interface JSX.IntrinsicElements exists", although it exists

TypeScript Version: 4.1.2

Search Terms:

no interface JSX.IntrinsicElements exists

Code

This works:

namespace JSX {
    export interface IntrinsicElements {
        div: {[k: string]: any}
    }
}

const d = <div /> // <-- no error
type test = JSX.IntrinsicElements // <-- no error

playground

But this does not work:

import type {JSX} from 'solid-js'

const d = <div /> // Error: JSX element implicitly has type 'any' because no interface 'JSX.IntrinsicElements' exists.

// But JSX is in scope, just that JSX expressions do not see it.
// As an example, the following non-JSX code works fine:
type test = JSX.IntrinsicElements // <-- no error

playground

Expected behavior:

It should see the definition of JSX.IntrinsicElements which is clearly present due to the import statement.

Actual behavior:

The JSX definition in the second example (imported) appears to be invisible to TS.

As you can see from both examples, only a locally-defined JSX works, but not one that is imported.

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Comments: 24 (7 by maintainers)

Most upvoted comments

@weswigham @RyanCavanaugh Mind re-opening the issue? The OP describes the issue exactly, there is no need for a new one. But I can make a duplicate if you confirm that would be better.

Right, Global though is basically a mess. You can’t have multiple JSX providers in the same project using Global they attack each other and it is completely broken. In those scenarios it is basically a non-starter.

I strongly dislike the assumption JSX has a single factory function. It is very narrow view that isn’t true. From Inferno’s precompiled VDOM, Solid’s custom DOM transformations… It is not a coincidence these also happen to be the most performant JSX libraries. You will see more optimizations of this nature in the future for performance reasons. Guaranteed. I would expect similar from Vue at some point and I wouldn’t count React out in the future.

There are plenty of things that JSX can generate and do. So jsxImportSource not being attached to a factory is a huge step forward to remove the last of the global JSX as it gets adopted. I’m fine with that generally but there are some obvious inconsistencies which @trusktr brings up. I’m not sure the best way though since the default would be to apply it across the project without any per file intervention. The challenge is that jsxImportSource only means something terms of the new Babel translation so coming from a clean slate perspective it’s not obvious why. The next time Babel changes (like they just did) so will TypeScript.

So it could be beneficial to have a native TypeScript solution, but I do understand if the plan is just continue keeping this perspective and follow Babel’s lead. Interestingly Babel itself does a pretty good job following the spec in terms of their syntax support, whereas TypeScript has historically tuned the implementation to match React.

I appreciate your consideration on this issue. At this point I’m fairly used to having most of the ecosystem assume JSX === VDOM factory function because Babel/TypeScript ship with a default JSX transform. But the spec for JSX makes no mention of what the output should be only indicates the syntax. I’m happy to have any solution at all. Even it is simply from the fact that React decided one day to have a new version that doesn’t rely on a pragma import. The pre 4.1.1 solutions were horrendous, so I will happily follow predefined file paths for types if it accomplishes the goal. I understand since you did not invent the syntax for jsxImportSource you can’t really change its function, but importing JSX working seems like it could be reasonable.

I strongly dislike the assumption JSX has a single factory function

I agree. This discludes other forms of JSX that do not have factory functions (like @ryansolid’s Solid.js is quite amazing), and it means they would have to define a fake type for something that doesn’t actually exist.

The next time Babel changes (like they just did) so will TypeScript.

Disregarding that, I really don’t see why we can’t just have jsx:preserve along with import {JSX} from 'anywhere', which would be intuitive and following standards that the ES community already has, and be on our way.

If import worked, I wouldn’t have spent multiple days fiddling to try and get JSX segregation working.

@weswigham Have you used more than one form of JSX in a single project before? It is a real big pain.

I believe import {JSX} from 'anywhere' should at least be supported, regardless of the other stuff. This would greatly simplify things.

@RyanCavanaugh I think the OP describes the issue fairly well. I don’t think a new issue would be needed.

Note, Babel’s new jsx-runtime feature (which also does not follow ES Module standards by virtue of requiring a particular import path) is specifically for libraries with runtime factory functions, largely guided by React, and ignoring non-factory JSX libraries and frameworks.

This is unfair to the rest of the JSX ecosystem, and TypeScript is not providing a more fair and desirable playground for other JSX libraries.

React incubated JSX, a language spec, not a runtime spec.

jsxImportSource requires a jsx-runtime.d.ts file at the root of a package. This coupling of JSX types to a specific filesystem structure is not ideal at all.

We should be able to

  • write "jsx": "preserve" in tsconfig.json (or specify a jsxFactory for compile output)
  • import type {JSX} from 'anywhere' in any file (or import it from a file that makes it global, which already works), or specify "jsxImportSource": "anywhere" (without the implied jsx-runtime path) to have it auto-imported in all files.

and be done.

I can’t think of a good reason to have this stuff coupled to specific files or coupled to React.

The “runtime” in jsx-runtime doesn’t make any sense for libraries that want to use jsx:preserve, as there isn’t even a runtime for TypeScript to care about.

After fixing jsxImportSource to not require some particular file path, it will make more sense because:

  • if jsx:preserve is used along with jsxImportSource, then one can expect that it will import only type definitions (which apparently is not documented anywhere to begin with)
  • if jsx:react or jsxFactory or similar are used, then the runtime can be expected to also be imported from jsxImportSource

But it would be best to simply allow import {JSX} (or import {JSX, jsx}, or import {JSX, h}, depending on tsconfig options) in any file, allowing users to use import instead of jsxImportSource, being more closely aligned with standards and conventions.

namespace JSX {
    export interface Element {
//  ^^^^^^
        foo: number
    }
    export interface IntrinsicElements {
//  ^^^^^^
        div: HTMLDivElement
    }
}

const d = <div></div>

@trusktr Can you please log a new concrete suggestion issue for supporting that? Piling on more comments to something that was filed as a bug is very confusing for us; if we want to have a real discussion on that point it’s very awkward that we’d have to scroll down n comments to understand what the issue was really about.

Besides when @jsxImportSource comments aren’t working (I posted a reproduction there),

I realized that we have to import JSX twice in order to use it in both normal code, and with JSX expressions. This is WET instead of DRY.

So in order for the following code to work, we must import JSX in the same file using both syntaxes:

/* @jsxImportSource solid-js */
// ^ FIRST IMPORT

import { createState, onCleanup, Component, JSX } from "solid-js";
// ^ SECOND IMPORT

import { render } from "solid-js/web";

const App: Component = () => {
  const [state, setState] = createState({ count: 0 }),
    timer = setInterval(() => setState("count", (c) => c + 1), 1000);
  onCleanup(() => clearInterval(timer));

  // The @jsxImportSource comment is needed for this JSX expression to recognize the <div> intrinsic element.
  return <div>{state.count}</div>;
};

render(() => <App />, document.getElementById("app"));

// The `import {JSX} from "solid-js"` is  needed in order to use the JSX types in non-JSX expressions
interface Foo extends JSX.HTMLAttributes<HTMLDivElement> {}

Try it here live in your browser, or clone the repo, and run npm install && npx tsx --noEmit and it will pass type checks. But if you remove either of those imports, one part of the code, or the other, will have a type error.

I hope by now you may be convinced that import {JSX} for both cases is more intuitive, more DRY, and probably less likely to not work (f.e. playground would just work).