Repository navigation
Type JSX elements based on createElement function #14729
Description
TypeScript currently uses a JSX namespace to type the props of JSX elements. This is customisable by modifying JSX.IntrinsicElements, JSX.ElementClass. However, the type of an element is always JSX.Element.
Of course more interfaces could be added to the JSX namespace. However, I think that with less effort, the compiler could be made more flexible. I'd propose to check JSX elements based on a (virtual) call to JSX.createElement. For instance, the current behaviour can approximately be written like this:
namespace JSX {
// intrinsic elements
function createElement<K extends keyof JSX.IntrinsicElements>(tag: K, props: JSX.IntrinsicElements[K], children: JSX.Element[]): JSX.Element;
// class and functional components
function createElement<P>(component: (JSX.ElementClass & { new(): { props: P } }) | ((props: P) => JSX.Element), props: P, children: JSX.Element[]): JSX.Element;
}Given that function signatures are well customisable with the use of generics for instance, most requests can be implemented this way. For instance, #13890 and #13618 would benefit from this change. Libraries that use JSX, but not on the React-way, will benefit from this too.
Proposed semantics
For a JSX element, a virtual call to JSX.createElement is constructed. It is passed three arguments:
- Tag name (string) in case of an intrinsic argument (
<div />). Otherwise, the identifier is passed (Foofor<Foo />). - An object containing the props.
- An array containing the children.
This should be roughly the same as the JSX transform used in the emitter phase. One notable difference is the following: in case of no properties or no children, an empty object or an empty array should be passed, instead ignoring the argument. This makes it easier to write JSX.createElement for library authors.
Backwards compatibility
For backwards compatibility, one of the following approaches could be taken:
- Use old logic if
JSX.createElementdoes not exist. - If
JSX.createElementdoes not exist, default it to (roughly) the definition above.
Error reporting
When type checking the generated function call, the checker can give error messages like "Argument of type .. " or "Supplied parameters do not match any signature of call target". These messages should be replaced when checking JSX elements.
Activity
RyanCavanaugh commentedon Mar 20, 2017
I think this is doable and could potentially cut out a lot of code from the compiler. When we added JSX support we didn't have spread types or
keyof/T[k]so this was impossible. Behavior-wise I think this approach would allow all current behavior to be represented with the exception ofdata-andaria-, which would could special case to not present excess property errors.The remaining difficulty is just technical - the overload resolution code is hardcoded in a lot of places with the assumption that it's operating over an actual argument list syntax tree rather than some abstract set of parameters.
niieani commentedon Mar 22, 2017
Having this would allow one to utilize the concept of generalized JSX with TS. Currently it seems impossible, because the return type of any JSX expression is always a non-generic
JSX.Element.In fact, given this it would make sense to ditch the whole "JSX" global namespace and just use the declared type of the locally scoped
jsxFactory(set in thetsconfig.json), so that the return type of a JSX expression could be different in every file, based on the implementation of thejsxFactory.ivogabe commentedon Apr 4, 2017
That's exactly what I thought!
The compiler already handles different kinds of function calls.
resolveSignatureinchecker.tsworks with normal call expressions, but also tagged templates and decorators. Those last two don't have an explicit argument list in the source code, so I think that JSX components could be integrated there too? Actually, that function already handles JSX elements for the language service, when used with SFCs.Bazyli Brzóska (@niieani) That's an interesting use case. I think that it would be useful in a lot of situations, other than the DOM, where some domain specific language is desired.
niieani commentedon Apr 6, 2017
Ivo Gabe de Wolff (@ivogabe) yeah, I especially like the example with ASTs, represented by JSX. So much more readable than declaring plain objects for AST.
ceymard commentedon May 15, 2017
I'd like to ping this issue ; could it be possible that the whole jsx mechanic be simply a transform to a function call with its implied checking ?
This would allow for so much flexibility in library creation.
RyanCavanaugh commentedon May 15, 2017
No. Two reasons and there may be more:
data-fooandaria-barare special, but we'd incorrectly flag these as errors[1]
niieani commentedon May 16, 2017
Ryan Cavanaugh (@RyanCavanaugh) The first could be fixed via other types of constructs that could be added to the language:
WithDomProperties<Point>-- adds typingdata-*andaria-*to the typeSecond we could think about. Maybe the spreads in JSX would behave differently?
There's probably something we could do. In any case, this would massively simplify the API and allow for custom JSX applications as I've mentioned in the comment above.
amir-arad commentedon Jun 29, 2017
AFAIK it's not just
data-*andarea-*, it's*-*.joewood commentedon Dec 12, 2017
Ryan Cavanaugh (@RyanCavanaugh) now that the second point has been fixed, can this be revisited?
jchitel commentedon Feb 6, 2018
Another problem to consider here is how to deal with backwards compatibility, particularly around the DefinitelyTyped definitions for React, which would have to be updated to support this.
Existing types that depend on the JSX namespace would no longer work because the compiler wouldn't look there anymore. The most graceful way I can see to handle that would be to modify the React definitions so that the JSX namespace definitions are dependent on (or at least structurally equivalent to) whatever types are used for
React.createElement, and deprecate the JSX namespace.For non-React use cases, this will probably just have to be a breaking change, with a suggested migration pattern. If someone is already using the JSX namespace for typing JSX, all they need to do is add a
jsxFactoryfunction (which they probably already have) and tie their JSX types to it.I agree that having this would improve the flexibility of JSX-in-TS considerably, but the migration for existing code will probably be quite painful.
mhegazy commentedon Feb 13, 2018
should be covered by #21699
40 remaining items
TylorS commentedon Mar 2, 2023
I've been following various JSX threads over the years all boiling down to this, I really truly hope this lands in TS at some point even if not 5.1 directly.
In response to this question, I sincerely hope for it to be 1. The type-safety of these functions seems most important over performance issues. The Effect community (formerly fp-ts) and I are building libraries today for html rendering and are today forced to use tagged template literals to be able to track typed resources and typed errors of children to be able to aggregate the resources required to run them and the errors they might produce as unions.
If JSX uses createElement as any other function for type-checking, we'd be able to utilize JSX's AST to further optimize them using compiler techniques and provide much more type-safe APIs for properties which is a lot more cumbersome to do with template literal strings and requires language service/server plugins to work as expected.
TylorS commentedon Apr 14, 2023
Ryan Cavanaugh (@RyanCavanaugh) You closed this with #51328, but it doesn't at all lookup the types by the signature of createElement. I think this should remain open
yairopro commentedon Jun 14, 2023
I just add here a pure sample code of the problem:
https://codesandbox.io/s/jsx-element-loses-types-6nmh3m?file=/src/App.tsx
arthurfiorette commentedon Sep 18, 2023
This was planned for milestone 5.1. Any updates here?
frzi commentedon Apr 25, 2024
Is there still any interest or plans for this? This seems like an incredible feature that’ll make libraries utilizing JSX far more versatile. 😄
robbiespeed commentedon Apr 26, 2024
Here's a couple TS Playground examples of how we could use this feature to enable similar features to flow's "renders" types and limiting allowed children or other props that receive jsx:
WorldMaker commentedon Oct 3, 2025
I've been thinking about this some this week and hitting some of the hard limits of the current namespace-based approach to JSX types. I've been exploring an architecture where different types of JSX functions get "dependency-injected" depending on runtime need or downstream usage of the JSX. Runtime support for it works just fine (using the original
"jsx": "react", "jsxFactory": "jsx"where you have to make sure your jsx function is in scope yourself), but I'd also love to get the types as rich as possible. At the moment I am struggling to get any types working via the ambient namespace tooling, and ideally it would be nice if the JSX types could be themselves captured in a way that is "dependency-injectable" (a part of the type of thejsxfunction itself.Say that I have an interface:
Even if that
jsximport is well-defined and included the ambient namespaces specific to it when imported directly, the ambient namespace isn't considered part of thetypeofand attempting to use thisjsxfunction results in type errors:I think I can set a single global JSX namespace (though that doesn't seem to be working right now for me either when I try the obvious things; ambient namespaces seem so far behind the times now on type import/export), but it would be nice to maybe also vary Element based on the generic parameter to the "Context" object (or something to that effect) or even swap IntrinsicElements (such as for a specialized context for SVG instead of HTML, for instance).
It's possible my intended architecture is just beyond the scope of intended JSX usage in Typescript, but I thought I'd bring it up if it sparked further ideas on improving how JSX is typed in general in Typescript, and what this issue suggests of trying to get more of the type information directly on the createElement function sounds close to a path forward that I could use.