-
Notifications
You must be signed in to change notification settings - Fork 31
Description
I may have misconfigured something (my jsx runtime settings or tsconfig) or misunderstood how to use the API, but it seems that JSX fragments (<>...</>) are typed as React.ReactElement, which seems to be defined here as (HTMLElement | SVGElement) :
Line 61 in a5682a5
| export type ReactElement = HTMLElement | SVGElement |
But at runtime they are DocumentFragment (which is a subclass of Node and not Element or HTMLElement/SVGElement).
This issue might be caused by or at least related to microsoft/TypeScript#21699, (which you have mentioned in the readme), but I wasn't sure. If this is the case, please disregard this issue.
My config
I am using the React 17+ style "automatic import", with the following in my tsconfig.json:
{
"jsx": "react-jsx",
"jsxImportSource": "jsx-dom/min",
}Maybe I need to use the older jsxFactory/jsxFragmentFactory instead of the newer automatic JSX runtime import? But I think these control the emitted code, not necessarily the type-checking behavior. Looking through the TypeScript docs I have not been able to find anything about setting the type for fragments. I thought there might be a way to configure it within the declare namespace JSX { ... } but have not found a way.
Not sure if this is relevant because my issue is with type-checking not runtime, but I am using typescript with noEmit, and esbuild as the bundler, which finds its configuration in the tsconfig.json "jsx" and "jsxImportSource" and all seems to work correctly.
Runtime behavior (works as expected)
Despite the incorrect typings, fragments actually do work at runtime because from what I can tell, this line detects a DocumentFragment as an Element and then appends it (which works fine with DocumentFragments just like most other Node subclasses except Document I guess).
Lines 220 to 221 in a5682a5
| } else if (isElement(child)) { | |
| appendChildToNode(child, node) |
This is due to the definition of
isElement which in terms of the DOM class hierarchy, actually checks that the argument is a Node (i.e. superclass of Element). Lines 13 to 15 in a5682a5
| export function isElement(val: any): val is Element { | |
| return val && typeof val.nodeType === "number" | |
| } |
Technically it checks that the argument is of type
{nodeType: number}, to which Node is assignable, along with all its subclasses including Element and DocumentFragment, so I understand why this code was written like this. But it allows DocumentFragments to slip through as Elements when the JSX fragment syntax is used.
(As a side issue maybe this function should be changed to function isNode(val: any): val is Node to avoid future bugs/confusion? but this is not an issue for me as it seems to be a private API, and works fine for its intended use, so I'm not complaining about this, just pointing this out).
Summary
So I'm just wondering if anyone knows how to correctly configure this on my end, or has any thoughts about strategies/techniques for ways to change jsx-dom to better support fragments with correct TypeScript types, particularly with the new-style automatic JSX runtime.
And also whether it would be worth changing the type of JSX.Element or ReactElement to include DocumentFragment? -- but this could potentially cause a bunch more issues, require changing some other parts of the jsx-dom API, and could be a big breaking change, so I'm not sure if this is the way to go.
Having components (functional or class-based) return a DocumentFragment at runtime, but with the type-checker thinking its a HTMLElement | SVGElement works in many simple cases such as appendChild but could obviously lead to bugs/inconsistencies - and also confuse developers if they don't dig into the jsx-dom code and figure out what is actually returned when they use JSX fragments. But in any case, the JSX literals are already a bit of a black box due to the aforementioned TypeScript issue, so I guess to be robust, it is worth checking the type of the JSX literals is what you expect at runtime and using type guards/casts as needed.
Its possible this has already been considered/discussed and the current behavior chosen as the best compromise, which would be fine because as I said, it works at runtime.
Thanks for this great library.