Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
trusktr opened this issue Dec 4, 2020 · 24 comments
Assignees
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@trusktr
Copy link
Contributor

trusktr commented Dec 4, 2020

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.

@trusktr
Copy link
Contributor Author

trusktr commented Dec 4, 2020

If libraries could export non-global versions of JSX types, then projects could very easily have a mix of files where each file may have a different type of JSX (f.e. React in some files, Preact in others, and Solid.js in others, assuming those libs export non-global JSX types).

@RyanCavanaugh RyanCavanaugh added the Question An issue which isn't directly actionable in code label Dec 4, 2020
@RyanCavanaugh
Copy link
Member

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

const d = <div></div>

@weswigham
Copy link
Member

If libraries could export non-global versions of JSX types, then projects could very easily have a mix of files where each file may have a different type of JSX (f.e. React in some files, Preact in others, and Solid.js in others, assuming those libs export non-global JSX types).

They can. preact does this.

@trusktr
Copy link
Contributor Author

trusktr commented Dec 4, 2020

Ahh, thanks! I never use namespace; I totally overlooked export.

@trusktr trusktr closed this as completed Dec 4, 2020
@trusktr
Copy link
Contributor Author

trusktr commented Dec 7, 2020

Turns out it actually does not work when JSX is imported, as any person organizing their code will want to do (rather than sticking everything in a single file):

This does not work:

// WHILE THIS WORKED
// namespace JSX {
//     export interface Element {
//         foo: number
//     }
//     export interface IntrinsicElements {
//         div: {foo: number, bar: string}
//     }
// }

// THIS DOES NOT
import type {JSX} from 'solid-js'

const d = <div foo={123} bar="string"></div> // JSX element implicitly has type 'any' because no interface 'JSX.IntrinsicElements' exists.

playground

We can verify JSX.IntrinsicElements does exist by adding this line to the example and hovering on it:

type test = JSX.IntrinsicElements // The type exists!

It doesn't seem right for JSX.IntrinsicElements to have some sort of special treatment in terms of how it is discovered. The rule with JSX.Anything should be the same as with any other types: look up JSX.Whatever in the current lexical scope just like with any other type.

Note, JSX.IntrinsicElements is fully visible in any non-JSX code that has it in scope. JSX.IntrinsicElements is only invisible to JSX expressions.

A solution in 4.1.1 is to use jsxImportSource in every file where the JSX types are needed. Why do we need a new syntax when we already have perfectly working import type {JSX} ... syntax?

@trusktr trusktr reopened this Dec 7, 2020
@trusktr
Copy link
Contributor Author

trusktr commented Dec 7, 2020

Please note, the JSX type from solid-js is in a .d.ts file, and in that case TypeScript apparently does not require export inside the namespace JSX definition. TS requires export inside the namespace when the file is a module (the inconsistency is confusing).

I also tried converting that file to a .ts file, and adding export for each type inside the JSX definition, but the error remains the same (the consumer code can not find the JSX.IntrinsicElements interface).

@weswigham
Copy link
Member

You wanna use the @jsx pragma to change the factory function used from React.createElement to whatever the factory for your library is. The JSX factory is looked up relative to the factory specified. So if your factory is h (eg, via a /* @jsx h */) we look for h.JSX.

@trusktr
Copy link
Contributor Author

trusktr commented Dec 7, 2020

Thing is, I don't want any jsx pragma. I'm using preserve, and transpile JSX with a custom step. It does not use factories.

But still, JSX types are in scope, and TS is not picking it up. I don't think there should be any difference between an inline declaration and an import with regards to visibility. JSX shouldn't have special treatment.

@trusktr
Copy link
Contributor Author

trusktr commented Dec 7, 2020

We have an existing import syntax, and I think it is very intuitive for that to work.

There is a new /* jsxImportSource ... */ syntax specifically for importing JSX types coupled with a factory. That is not intuitive: people don't assume to import all types with import except one special type (JSX) that requires comment syntax.

I think that

import {JSX} from `somewhere`
// or
import type {JSX} from `somewhere`

should just work. I really feel there isn't a need to make JSX a specially-treated type when it comes to making it visible in a scope.

@RyanCavanaugh RyanCavanaugh added Working as Intended The behavior described is the intended behavior; this is not a bug and removed Question An issue which isn't directly actionable in code Working as Intended The behavior described is the intended behavior; this is not a bug labels Dec 7, 2020
@RyanCavanaugh RyanCavanaugh added this to the TypeScript 4.2.1 milestone Dec 7, 2020
@RyanCavanaugh RyanCavanaugh added the Needs Investigation This issue needs a team member to investigate its status. label Dec 7, 2020
@weswigham
Copy link
Member

You can still use the pragma with preserve, I believe - we need the name of the factory function to know under which non-global namespace to look for the JSX namespace.

@weswigham
Copy link
Member

(Also, we didn't "invent" the comment pragma syntax - we just copied it from babel)

@trusktr
Copy link
Contributor Author

trusktr commented Dec 15, 2020

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.

@trusktr
Copy link
Contributor Author

trusktr commented Dec 16, 2020

TypeScript playground does not install dependencies for jsxImportSource. playground example

If we could instead rely on import, the playground example (which will install solid-js based on the import) would just work already.

@trusktr
Copy link
Contributor Author

trusktr commented Dec 16, 2020

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).

@trusktr trusktr changed the title Error: no interface JSX.IntrinsicElements exists, although I've defined it Unable to import JSX types, Error: "no interface JSX.IntrinsicElements exists", although it exists Dec 16, 2020
@trusktr
Copy link
Contributor Author

trusktr commented Dec 17, 2020

@weswigham I updated the OP to make it clear based on the new findings regarding import.

@ryansolid
Copy link

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.

@weswigham weswigham added Working as Intended The behavior described is the intended behavior; this is not a bug and removed Needs Investigation This issue needs a team member to investigate its status. labels Jan 6, 2021
@weswigham
Copy link
Member

We do two things to find JSX types:

  1. Look for <root identifier of JSX factory entity name>.JSX - IMO, this is preferred, as it is local to a file, and customizable on a per-file, and per-JSX package basis. However, it is also newer (and was not in our original JSX implementation), so fewer packages/authors support it.
  2. Look for a global JSX namespace. This was our original implementation behavior, done as such because all JSX types are essentially compiler-intrinsic changing types that set global behaviors. We had no concept of file-local JSX behavior.

We have never looked for a locally scoped namespace named JSX, nor do I think we should further diversify what strange things you can do to typecheck JSX. We already have a mechanism for locally-sourced JSX definitions - if you can, I recommend using it.

@ryansolid
Copy link

ryansolid commented Jan 6, 2021

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.

@trusktr
Copy link
Contributor Author

trusktr commented Jan 18, 2021

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.

@trusktr
Copy link
Contributor Author

trusktr commented Jan 18, 2021

The @jsxImportSource feature is also lacking.

  • There is no hovering on it to see any intellisense tooltips.
  • With import we can hover on the imported identifiers and see information.
  • With import we can Go To Definition on imported identifiers or the module specifier.
  • We cannot auto-complete @jsxImportSource within an comment.
    • We can auto-complete @ts-* comments, just not @jsxImportSource comments.
    • We cannot auto-complete module specifiers in @jsxImportSource comments.

It really makes much sense to support import {JSX}.

nor do I think we should further diversify what strange things you can do to typecheck JSX.

You should honestly remove the strange things.

But even if you keep them, import works in an obvious way and with benefits; it is not strange, and people know what to expect from it, and people who know import/export syntax will not need to read any documentation, they can guess to import {JSX} simply with intuition.

The very first thing I thought was: I'll just import JSX.

It makes so much sense!

@trusktr
Copy link
Contributor Author

trusktr commented Jan 19, 2021

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.

@RyanCavanaugh
Copy link
Member

@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.

@trusktr
Copy link
Contributor Author

trusktr commented Apr 11, 2021

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

@trusktr
Copy link
Contributor Author

trusktr commented Dec 2, 2021

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug
Projects
None yet
Development

No branches or pull requests

4 participants