hast-util-to-jsx-runtime icon indicating copy to clipboard operation
hast-util-to-jsx-runtime copied to clipboard

Base types on generics, not global JSX namespace

Open remcohaszing opened this issue 1 year ago • 16 comments

Initial checklist

  • [x] I read the support docs
  • [x] I read the contributing guide
  • [x] I agree to follow the code of conduct
  • [x] I searched issues and couldn’t find anything (or linked relevant results below)
  • [x] If applicable, I’ve added docs and tests

Description of changes

For a while now, JSX frameworks don’t have to declare the global JSX namespace anymore. This is a significant improvement when using multiple JSX frameworks in the same project. React has deprecated the global JSX namespace. Preact doesn’t even define it anymore.

This change replaces the use of the JSX namespace with generics, to make it compatible with modern JSX frameworks. I also have an alternative branch locally which accepts only one generic, which is the type of the jsx function.

This removes the inference of props in custom components. I’m not sure what the impact is of that.

This could be considered a breaking change on the type level.

Closes #4

remcohaszing avatar Feb 16 '24 13:02 remcohaszing

I also have an alternative branch locally which accepts only one generic, which is the type of the jsx function.

that sounds pretty nice 🤔 why not?

wooorm avatar Feb 16 '24 16:02 wooorm

When did this change? Which TS versions can(not) work without JSX?

Only really old TypeScript versions can’t work without the global JSX namespace. The ${jsxPragma}.JSX namespace has been around for a while. I think it slightly predates the automatic runtime. But it took a long time for JSX frameworks to actually use it. Now it appears frameworks are moving towards doing so.

Although I recommend against using the global JSX namespace, for @types/mdx this was convenient. Thankfully there’s a workaround, for example for Preact (I need to confirm this and PR DefinitelyTyped to update the docs):

declare module 'mdx/types' {
  import { JSX } from 'preact/jsx-runtime'
}

I also have an alternative branch locally which accepts only one generic, which is the type of the jsx function.

that sounds pretty nice 🤔 why not?

It does sound nice! I ran into some JSDoc limitations though, that make it impossible to make this work without converting toJsxRuntime to TypeScript. Also it feels kind of weird to infer the development runtime types based on typeof production.jsx.

remcohaszing avatar Feb 20 '24 12:02 remcohaszing

I was about to open an issue requesting that the types (FunctionalComponent, etc) from ./components.js be exported by index.d.ts. Why are those being removed?

shellscape avatar Feb 20 '24 14:02 shellscape

Those types depend on the global JSX namespace, which is being deprecated / removed by some frameworks. I recommend using framework specific types (i.e. ReactElement, ElementType, FunctionComponent, etc. from @types/react. If you really need framework agnostic types, I suggest you use @types/mdx. Those types also take JSX.ElementType into account, wich the types removed in this PR do not.

Note that you only need framework agnostic JSX types if you write something where those types become part of the public interface of a library. You can likely use @types/react instead.

remcohaszing avatar Feb 20 '24 14:02 remcohaszing

Note that you only need framework agnostic JSX types if you write something where those types become part of the public interface of a library. You can likely use @types/react instead.

That's exactly what I'm doing. I'll have compat between React, Preact, and SolidJS when done.

shellscape avatar Feb 20 '24 14:02 shellscape

But will those JSX based types be part of the public interface?

Anyway, that should also be possible with the generics solution proposed in this PR.

remcohaszing avatar Feb 20 '24 15:02 remcohaszing

But will those JSX based types be part of the public interface?

Yes.

Anyway, that should also be possible with the generics solution proposed in this PR.

Would love to see an example using the changes in the PR

shellscape avatar Feb 20 '24 15:02 shellscape

${jsxPragma}.JSX

It sounds like we could use that registry here to type which components are allowed? Why not use that?

toJsxRuntime to TypeScript

Curious to hear more about those limitations.

Also it feels kind of weird to infer the development runtime types based on typeof production.jsx.

a) we might be able to support both? b) many runtimes do jsxDEV = jsxs = jsx, and, I think there’s some sort of “contract” that they should yield the same things

wooorm avatar Feb 27 '24 10:02 wooorm

${jsxPragma}.JSX

It sounds like we could use that registry here to type which components are allowed? Why not use that?

That was for the classic runtime, but even if we used that, we can’t access that. For the automatic runtime, we need the JSX namespace imported from ${jsxImportSource}/jsx-runtime.

toJsxRuntime to TypeScript

Curious to hear more about those limitations.

I did get it to work after all. Default type parameter values behave somewhat different in types in JSDoc. This was confusing.

remcohaszing avatar Mar 01 '24 16:03 remcohaszing

but even if we used that, we can’t access that.

I don’t understand. We can access it, but we can’t?

we need the JSX namespace imported [...]

Do you have an example of how that works? I don’t think I’ve seen Preact or so expose those things.

wooorm avatar Mar 01 '24 17:03 wooorm

but even if we used that, we can’t access that.

I don’t understand. We can access it, but we can’t?

It’s not possible to access types in a namespace from the typeof such a namespace. I.e. given a namespace…

namespace JSX {
  interface Element {}
}

… it is not possible to retrieve Element from typeof JSX. Nor is it possible to pass a namespace as a type parameter.

we need the JSX namespace imported [...]

Do you have an example of how that works? I don’t think I’ve seen Preact or so expose those things.

import { JSX } from 'react/jsx-runtime'
//       ^^^ We would need this value
//                   ^^^^^ But this part is dynamic in our case.

So we cannot infer anything from the JSX namespace. We were only able to do so previously, because frameworks were using the global JSX namespace.

remcohaszing avatar Mar 18 '24 12:03 remcohaszing

We were only able to do so previously, because frameworks were using the global JSX namespace.

But like. Which frameworks have removed them? Are frameworks going to? Because supporting components is quite nice here? Not sure I’d want to see it removed?

TS itself can validate whether x-y is a valid element and whether the props passed to it are fine. Why can’t we?

Take https://github.com/preactjs/preact/blob/a2c12f5a46a70b2b58517f5e14e731a77d6d64a3/test/ts/custom-elements.tsx#L3 for example.

I find this really hard to review: there are no sources cited for all of this.

wooorm avatar Mar 18 '24 14:03 wooorm

The global JSX namespace is removed by preact, solid-js, and vue. For react it’s deprecated.

Unfortunately demonstrating this doesn’t work well in a playground. Instead, you can try with these files:

// package.json
{
  "type": "module",
  "dependencies": {
    "preact": "^10.0.0",
    "solid-js": "^1.0.0",
    "typescript": "^5.0.0",
    "vue": "^3.0.0"
  }
}
// tsconfig.json
{
  "compilerOptions": {
    "module": "nodenext",
    "jsx": "react-jsx",
    "jsxImportSource": "preact",
    "noEmit": true
  }
}
// index.tsx
// These imports exist to show none of them register a global `JSX` namespace anymore.
import 'preact'
import 'solid-js'
import 'vue'
import {/* JSX */} from 'preact/jsx-runtime'
import {/* JSX */} from 'solid-js/jsx-runtime'
import {/* JSX */} from 'vue/jsx-runtime'

type JSXElement = JSX.Element

const element = <div>hello</div>

The type of <div> depends on the value of jsxImportSource in tsconfig.json. You can try this by changing jsxImportSource and Ctrl + clicking the <div> element in the code. The type of <div> and element is based on the JSX export of the ${jsxImportSource}/jsx-runtime module. TypeScript can use the information of the JSX namespace, because it’s something built into TypeScript. As a library we can’t.

For TypeScript exporting the JSX namespace is the only thing needed to support the JSX automatic runtime. At runtime, the jsx, jsxs, and Fragment values are needed. Some runtimes export them, but not all. Perhaps the current state is best viewed as a compatibility table.

Feature @types/react preact solid-js vue
Global JSX namespace[^1] :warning: deprecated
Export JSX namespace[^2] ✔️ ✔️ ✔️ ✔️
Export jsx :warning: [^3] ✔️ ✔️
Export jsxs :warning: [^3] ✔️ ✔️
Export Fragment ✔️ ✔️ ✔️

[^1]: Currently supported by hast-util-to-jsx-runtime [^2]: Used by TypeScript. Not dynamicallyt by libraries such as hast-util-to-jsx-runtime [^3]: Exported, but very loose type with no possibility of inferring the types of props

So basically the current state is:

  • hast-util-to-jsx-runtime depends on a feature that’s deprecated in @types/react and unsupported by other frameworks.
  • The dependency on this feature means it’s a type error unless @types/react is used.
  • React is the worst framework to represent the current state, because it’s the most loose.
  • All frameworks listed support the JSX automatic runtime, but we can’t use the information they provide for that.
  • It could be possible to infer props for preact and vue, but it requires more types gymnastics for little benefit IMO. And TBH getting to the current state took me more time and energy than I had hoped.
  • Projects that use this library for one specific runtime, such as react-markdown, can easily specify types based on the specific JSX framework, without using the types provided by hast-util-to-jsx-runtime.

remcohaszing avatar Mar 18 '24 16:03 remcohaszing

The global JSX namespace is removed by preact, solid-js, and vue

Do you have references?


The docs still state that JSX is fine: https://www.typescriptlang.org/docs/handbook/jsx.html#intrinsic-elements. This seems like a decision that affects React on DT, instead of something that affects all TS users. Otherwise, there would be docs changes to TS too right? Or references issues on TS repos?

React is the worst framework to represent the current state, because it’s the most loose.

🤔 The current existing JSX namespace is pretty good? 🤔 You mean that the types of the jsx function are bad?

All frameworks listed support the JSX automatic runtime, but we can’t use the information they provide for that.

We could if folks pass the JSX types from their /jsx-runtime in?


OK, I think I understand the concerns. The JSX global is going away. Fine. That solves some things for some users. But. There is no good alternative. The types of jsx and such are often bad. We can’t really type components well. At that point. Why even do this PR? We could return unknown. Or indeed, require folks to pass in several JSX types.

wooorm avatar Mar 19 '24 10:03 wooorm

The global JSX namespace is removed by preact, solid-js, and vue

Do you have references?

This PR deprecates usage in @types/react https://github.com/DefinitelyTyped/DefinitelyTyped/pull/64464. There must be a commit somewhere that removes it for Preact, but I can’t find it quickly.


The docs still state that JSX is fine: https://www.typescriptlang.org/docs/handbook/jsx.html#intrinsic-elements. This seems like a decision that affects React on DT, instead of something that affects all TS users. Otherwise, there would be docs changes to TS too right? Or references issues on TS repos?

The docs don’t mention where the JSX must be specified. This used to be global, later support was added for ${jsxPragma}.JSX, e.g. React.JSX / React.createElement.JSX, but it took a long time for frameworks to implement this. This was convenient for @types/mdx. Later the automatic runtime was added.

I don’t even see a mention of added support for ${jsxPragma}.JSX in any release notes. I guess this is because it doesn’t affect most users, only authors of JSX frameworks.

React is the worst framework to represent the current state, because it’s the most loose.

🤔 The current existing JSX namespace is pretty good? 🤔 You mean that the types of the jsx function are bad?

I mean this in the sense that using Preact or Vue in tests would have shown the problem being solved here.


All frameworks listed support the JSX automatic runtime, but we can’t use the information they provide for that.

We could if folks pass the JSX types from their /jsx-runtime in?

Yes, that’s the intention of this PR.


OK, I think I understand the concerns. The JSX global is going away. Fine. That solves some things for some users. But. There is no good alternative. The types of jsx and such are often bad. We can’t really type components well. At that point. Why even do this PR? We could return unknown. Or indeed, require folks to pass in several JSX types.

What do you mean by We could return unknown. Or indeed, require folks to pass in several JSX types.? As I see it, that’s what this PR does. It infers the types based on generics.

remcohaszing avatar Mar 21 '24 11:03 remcohaszing

This PR deprecates usage in @types/react https://github.com/DefinitelyTyped/DefinitelyTyped/pull/64464. There must be a commit somewhere that removes it for Preact, but I can’t find it quickly.

Yeah I know the one where DT maintainers change something in the React types. But there is no discussion about it. You make a lot of statements about how things are and that Preact/Vue/etc changed something. But there’s nothing backing that up yet.

This used to be global

Right, so I miss sources for such statements. There’s only a change in DT for React. Nothing on the TS part. Or other frameworks. And, these docs, they show the global JSX being used right? Otherwise it would explain something along the lines of “for React, use React.JSX, for vue, do vue.JSX”, etc.

Yes, that’s the intention of this PR.

As I see it, that’s what this PR does. It infers the types based on generics.

From where does it infer? From the jsx function right? But, we have established that practically, current frameworks type those functions badly. What if we either: a) require people to pass types in from React.JSX or equivalent (so React.JSX.Element, React.JSX.ElementClass, etc), b) bail and return unknown.

wooorm avatar Mar 21 '24 11:03 wooorm

I think this PR removes too many useful features.

JSX is great. JSX is used in millions of files. It’s super useful to type components and return values. See also https://github.com/syntax-tree/hast-util-to-jsx-runtime/pull/9#issuecomment-2407649418.

I would be open to discussing bare minimal components, or returning unknown if JSX.Element is not defined, stuff like that, to not error.

wooorm avatar Oct 11 '24 15:10 wooorm