@types/react/@types/react-native package and had problems.Definitions by: in index.d.ts) so they can respond.(I wrote a blog post about this issue here, but I assume anyone reading this is more familiar with TS, so I'll skip to the good parts.)
JSX validation uses the JSX.IntrinsicElements interface (defined on the global namespace).
Currently, the valid DOM (HTML/SVG) host components, their attributes, and DOM events are defined by @types/react. @types/react also extends the JSX.IntrinsicElements interface to validate these HTML/SVG host component. This means that any project that installs @types/react considers HTML/SVG JSX to be valid. For example:
import React from "react";
import { Text } from "react-native";
export default function MyComponent() {
return (
<div>
<Text>Hi!</Text>
</div>
);
}
I have two proposals to make. Both are breaking, but to various degrees.
The first proposal is to move anything DOM out of @types/react and into @types/react-dom. An ambient module can be used to extend the React namespace from within @types/react-dom. This would allow the React namespace to maintain the same API, but would require users to have @types/react-dom installed if they want to 1) have DOM JSX validated and 2) use DOM types.
The second proposal takes this a step further and would move the DOM definitions to the ReactDOM namespace. This change would still require @types/react-dom to extend React using an ambient module, but the brunt of the change would be moving DOM definitions out of the React namespace.
The second proposal would break a lot more code. I've implemented most of the first proposal (see here) and while it is a large change (it touches > 200 packages), it is relatively straightforward.
I personally prefer the second proposal. To me, it makes more sense to have DOM definitions come from a DOM namespace. However, the impact of the change is also daunting.
I'm curious to know what everyone else thinks about these potential changes.
import React from "react";
import { ButtonHTMLAttributes } from "react-dom";
export default function UglyButton(props: ButtonHTMLAttributes) {
const style = {
color: "red",
background: "green"
}
return (
<button style={style} {...props} />
);
}
(#24433 is similar, but hasn't seen any discussion in the ten months since it was posted, which is why I started a new issue.)
DefinitelyTyped doesn't follow semver, but that being said it's most definitely an extreme breaking change even though the actual fix is not that difficult. Perhaps if we were to supply a codemod, it'd make it easier for folks to transition?
@ferdaber To clarify, are you referring to the second proposal? Or is there a breaking behavior besides installing @types/react-dom with the first proposal?
I agree with moving the DOM definitions to react-dom, and even with moving the interfaces to ReactDOM. Don't take me at face value though because I'm _really_ open to breaking changes.
I do want to keep React.ComponentPropsWithoutRef<'button'> working, though, and users are supposed to use _that_ instead of ButtonHTMLAttributes. Not because of backwards compatibility, I just think it's a much better abstraction than using our internal interfaces directly.
PS: I wonder if we can use this opportunity to make it easier / possible for a future react-native-web type definition to not bring in the react-dom JSX.IntrinsicElements. Maybe a separate type-only entry point that doesn't include the JSX.IntrinsicElements augmentations. The hard part is doing that and keeping React.ComponentProps working.
I made a PR for this and the first thing I have to say to anyone who wants to review it is this: Sorry!
For the most part, this went smoothly. Most changes are 1) switching to the ReactDOM namespace in definitions and 2) using <reference types="react-dom" /> in tests that expect JSX.IntrinsicElements to include DOM host components.
I want to say that this is the right approach; most of the changes felt natural to me. There were a few things that I am unsure of, but I'll ask those questions over on the PR.
I'm afraid the real problem is that JSX namespace even exists. <div /> should (in the most common scenario) compile to React.createElement('div') and be typed by a definition of createElement. When we get rid of this unfortunate feature in TS, typings won't leak into other libraries.
The proposal sounds quite strange to me, given that JSX-constructed React elements are just React elements, and there is no requirement for them to ever become DOM nodes. Also I'm not a big fan of having to import react-dom in every single component next to react.
You won't have to because having react-dom types installed will kick in the JSX global namespace.
Technically you can pass in anything into React.createElement and it won't complain, but that doesn't mean we shouldn't restrict it. Every specific renderer of React should overload the createElement signature or add more into the JSX.IntrinsicElements namespace. With the base react types package contributing none of those signatures.
Technically you can pass in anything into
React.createElement
The whole point is that React.createElement has stricter type. JSX nodes have untyped props, their type is not even generic. Thus if you have a component that takes children only of a certain type, you cannot satisfy this requirement when they're constructed via JSX.
Every specific renderer of React should overload the createElement
How come a tree traversal overloads the type of tree node constructor?
or add more into the JSX.IntrinsicElements namespace
Sure, global variables were never an issue.
Agreed that createElement is stricter, but that change won't come anytime soon.
If we're typechecking against createElement for valid jsx constructors then we necessarily need to build in relaxed overloads than the base. The base signature needs to be basically nonfunctional since without a renderer React is unusable. I'm not entirely sure what you mean by tree traversal, but I was talking about renderer in React terminology as in ReactDOM vs ReactNative vs ReactART
My image of an ideal signature is only 2 overloads. One for variadic
children, one for explicit children parameter.
The generic argument "extends" should include only user-definable
components and something like "keyof HostElements" where "HostElements" is
an interface in the React module instead of in the JSX namespace. Importing a reconciler that has host elements (like react-dom) would augment that interface.
If things are done right it should be possible to mix different @jsx
pragmae and even different frameworks inside the same tsconfig.json project.
Unfortunately it's not possible to have something React that accepts DOM elements in one file and one that doesn't in another. This is for the same reason as it's not possible to model the presence of a Context.
We'd still run into the pitfall like you said of having the same factory function (createElement) for different renderers so we're still kind of stuck of being unable to differentiate between different files with different pragmae.
Doesn't the localized JSX namespace lookup already achieve the same effect though? Technically TS already tries to look at React.JSX instead of the global namespace so it'd be isolated inside the react module.
To be able to get a different React.createElement depending on the framework per file _in the same tsconfig.json_ you'd need something nearly equivalent to a Scala implicit. A module-local hidden variable that can be seen from another module without you passing it in.
The change though would allow just changing the @jsx pragma to a different constructor (e.g. Vue's) to get it to ignore React's host elements.
No need in implicits. It can be made quite explicit (pun intended):
// project-wide-react-wrapper.ts
export type PureComponent<P = {}, S = {}> = React.PureComponentFactory<ReactDom.BuiltInElements, P, S>`
// component.tsx
import {PureComponent} from './project-wide-react-wrapper.ts';
export class Smth extends PureComponent {
render() {
return /* only ReactDom.BuiltInElements and elements of user-defined components allowed here */;
}
}
React.createElement is stripped of all the renderer-specific knowledge for this approach to work. The only bad things that comes to mind is that error messages are going to be displayed on the whole return statement instead of the place where a mistake really happened.
(I still feel as if I'm missing something, and probably should take a nap)
That PureComponent exists in type only, cannot be augmented, and would need to be declared in every module that imports react. It's not that simple.
That also depends on you importing PureComponent by name, which you're not supposed to be able to do because react is not an ES module.
Most helpful comment
I agree with moving the DOM definitions to
react-dom, and even with moving the interfaces toReactDOM. Don't take me at face value though because I'm _really_ open to breaking changes.I do want to keep
React.ComponentPropsWithoutRef<'button'>working, though, and users are supposed to use _that_ instead ofButtonHTMLAttributes. Not because of backwards compatibility, I just think it's a much better abstraction than using our internal interfaces directly.PS: I wonder if we can use this opportunity to make it easier / possible for a future
react-native-webtype definition to not bring in the react-domJSX.IntrinsicElements. Maybe a separate type-only entry point that doesn't include theJSX.IntrinsicElementsaugmentations. The hard part is doing that and keepingReact.ComponentPropsworking.