hast-util-to-jsx-runtime
hast-util-to-jsx-runtime copied to clipboard
Base types on generics, not global JSX namespace
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
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?
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.
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?
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.
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.
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.
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
${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
${jsxPragma}.JSXIt 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 TypeScriptCurious 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.
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.
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.
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.
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-runtimedepends on a feature that’s deprecated in@types/reactand unsupported by other frameworks.- The dependency on this feature means it’s a type error unless
@types/reactis 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
preactandvue, 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 byhast-util-to-jsx-runtime.
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.
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
JSXis 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
jsxfunction 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
JSXtypes from their/jsx-runtimein?
Yes, that’s the intention of this PR.
OK, I think I understand the concerns. The
JSXglobal is going away. Fine. That solves some things for some users. But. There is no good alternative. The types ofjsxand such are often bad. We can’t really type components well. At that point. Why even do this PR? We could returnunknown. Or indeed, require folks to pass in severalJSXtypes.
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.
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.
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.