-
Notifications
You must be signed in to change notification settings - Fork 12.8k
JSX.Element and function component return type assignability #61620
New issue
Have a question about this project? No Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “No 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? No Sign in to your account
Comments
sufianrhazi
added a commit
to sufianrhazi/gooey
that referenced
this issue
Apr 26, 2025
This change is unfortunately necessary until either microsoft/TypeScript#61620 (a new issue reported by me) or microsoft/TypeScript#21699 (a longstanding old issue unlikely to be fixed) are resolved. TypeScript uses the JSX.Element type to determine the type of any JSX expression. It is not possible for two different JSX expressions to evaluate to different types. Gooey supports asynchronous components, **but** in order for this to work a component must be defined as something that returns a JSX.Element; so JSX.Element must be extended to become: RenderNode | (Promise<RenderNode> & Partial<RenderNode>) This extension of (Promise<RenderNode> & Partial<RenderNode>) means that in practice, users can easily call rendernode methods (like `.retain()`) via: const jsx = <div />; jsx.retain?.(); // ... jsx.release?.(); Which should always work, as the `createElement` jsxFactory function always returns a `RenderNode`. It's really unfortunate that the optional chaining operation is needed here. Hopefully when either of those issues are fixed, the JSX evaluation type can be separated from the function component return type.
sufianrhazi
added a commit
to sufianrhazi/gooey
that referenced
this issue
Apr 26, 2025
Bugfixes and typecheck fixes Changes since 0.22.0 - FEATURE: new dynMap() and dyn() convenience functions exported - BREAKING: JSX.Element now returns Partial<RenderNode>, which means if you were calling any RenderNode methods on JSX, you must now call them with optional chaining (i.e.: `jsx.retain?.()`). See microsoft/TypeScript#61620 - BUGFIX: fixed crashes when calculations had changes in their dependencies and were retained/unretained/destroyed in certain sequences
TypeScript 5.1 introduced |
Oh you're absolutely right, I completely missed this addition. Thank you, I'll close this issue. |
No Sign up for free
to join this conversation on GitHub.
Already have an account?
No Sign in to comment
🔍 Search Terms
"JSX function component return type", "jsxFactory return type", "function component types", "jsx expression types"
Note: this is related to the longstanding issue #21699 -- this issue would not be an issue if that issue was fixed. However it seems like that issue is prohibitively expensive, so I'm filing this issue as a performant workaround that would allow more flexible JSX types.
✅ Viability Checklist
⭐ Suggestion
I'd like a new
JSX
namespace type (similar toElementClass
) that allows framework authors to relax the constraint whereJSX.Element
is used as both for the type produced by evaluating JSX and as a constraint where component functions must return a type assignable toJSX.Element
.Essentially, I'd like to be able to do this:
As long as the
jsxFactory
function is guaranteed to return a type assignable toJSX.Element
, then functions should be able to be treated as function components if their return type is assignable toJSX.ComponentFunctionReturnType
.If omitted, the current behavior should be used where function components must return a type assignable to
JSX.Element
.📃 Motivating Example
I'm the author of a UI library called Gooey that uses JSX. It's unlike React and other frameworks that treat JSX expressions as black boxes. Instead, Gooey exposes methods on the
JSX.Element
type.It does something like this:
For example, here's some code that renders a piece of JSX that is moved to to one of two places in the DOM without recreating the underlying elements:
This is possible because the
JSX.Element
type isRenderNode
, a type that has the.retain()
and.release()
methods on it—and thecreateElement
factory returns thisRenderNode
type.Gooey also supports asynchronous functions as components. That is to say, you could write something like this:
And the component will initially render to nothing until the returned promise is resolved. However this does not typecheck.
The problem
TypeScript enforces that all function components must return a value that is assignable to
JSX.Element
, which means these async components will not typecheck correctly.This
MyAsyncComponent
is of type() => Promise<RenderNode>
, which is not assignable toRenderNode
.It's important to note that the JSX factory (
createElement
) function in Gooey always returns aRenderNode
instance, even when given a component function that returns a promise.If this assignability limitation is eased by setting
JSX.Element
to beRenderNode | Promise<RenderNode>
, then a DX problem is introduced: the result of evaluating a JSX expression will now give you aRenderNode | Promise<RenderNode>
. Which means ourMyTeleportationComponent
code breaks, since now callingjsx.retain()
will fail typechecking because the.retain()
method does not exist onPromise<RenderNode>
(even thoughcreateElement
is guaranteed to give you aRenderNode
).💻 Use Cases
As stated above, I'd like to write a framework where:
Promise<RenderNode>
RenderNode
jsxFactory
function always returns aRenderNode
, even when given a component function that returnsPromise<RenderNode>
RenderNode
It's not possible to do this right now.
For Gooey, I'm thinking about lying for the sake of ergonomics, and making
JSX.Element
be the type:RenderNode | (Promise<RenderNode> & Partial<RenderNode>)
This type allows
Promise<RenderNode>
to be assigned to itAnd it allows users to ergonomically call functions on this type via the optional chaining operator:
I'm not a big fan of this, as it forces conditionals to exist at runtime for the sake of ergonomics.
The text was updated successfully, but these errors were encountered: