Decouple jsx element type from jsx factory return type and sfc return type
weswigham opened this issue ยท 118 comments
We need to look up the type of a jsx expression by actually resolving the jsx factory call, so that we don't create a reference to the global JSX.Element type, which can change shape between react versions (as it needs to in the react 16 upgrade). We also need to resolve the sfc return type and class element type from the parameters of the factory function overloads for the same reasons, doubly so because the types allowable as render method and SFC return values are no longer the same as JSX.Element (namely, they can be strings, arrays, portals, etc).
This might be considered a breaking change, because some consumers may expect JSX.Element to always be a supertype of both jsx element expression return types and SFC return types (even though this isn't true in react 16) - we certainly made that assumption internally, hence the need for the change. ๐ฑ
@weswigham would addressing this address #18357 ? It'd be great to be able to correctly type children render props in React.
There are a lot of issues related to this that have been closed, saying that this issue will handle them. I am assuming that the case in #18357 will be handled by this as well.
At the core of this is the ability to type JSX based off of the createElement function, rather than using the set of interfaces in the JSX namespace. If that feature is indeed going to be implemented as part of this issue, then a natural extension would be that the children could end up anywhere in the resulting element, and they could be typed based off of anything, rather than just restricting children to the props.
@jchitel agreed. I just hit this snag trying to change the react typings yesterday for this exact same reason.
@weswigham If you haven't started on this yet, I'd like to take a crack at it
@ericanderson I actually started work on this today, sorry ๐
@weswigham Damn. Can you make sure I can capture the SFC or ComponentClass so we can do things like limit the children of <ButtonGroup> to <Button>?
@ericanderson That's the plan. (I mean, technically it was a bug report a long time ago that we closed as "fixed" even though it was only half-fixed)
I think that will also let us mark the defaultProps optional.
By defaultProps do you mean the intrinsic props?
I mean:
interface Props {
foo: string,
bar: number
}
class Foo extends React.Component<Props> {
static defaultProps = { foo: "Hi Mom!" };
//...
}In this world, the compiler should only require me to specify bar and foo should now be considered foo?: string
... maybe. Even with localized types looked up from the signatuers, we still have to make assumptions about how the custom ctor/sfc is effectively built, because there's actually two calls going on. The first call is the constructor for the class or the SFC, (which are expected to take props as an argument), and can be overloaded - this is where a lot of props validation actually happens. The second call is the factory function itself, which actually needs the reified types from the inner call to be typechecked correctly (And today isn't actually typechecked at all, which is the root of most of these issues).
TBQH a lot of the JSX machinery in-place today is to get higher-ordery behavior from a system that used to not support any. I think now that we have conditional and infer types, there's a good chance I may actually be able to desugar a lot of the magic currently applied to JSX to normal typesystem operations. I'm generally just going to look at improving this as much as is feasible ๐
So the props we should enforce are actually Omit<Props, typeof Foo.defaultProps> & Partial<typeof Foo.defaultProps>... which maybe we can't do with this change.
@weswigham Thanks!
I'm not entirely sure that this was technically fixed. #18131 was fixed, but we still don't actually look at what a given JSX factory invocation returns.
Correct.
What are the technical impediments to doing ReturnType<factoryFunction> in typechecking JSX?
...or do I have absolutely no idea how any of this stuff works.
ReturnType doesn't use overload resolution, nor does it apply type argument inference, both which utilize the actual props passed in to a JSX element.
So you'd potentially get an "under-instantiated" and/or broad type depending on if your JSX factory is generic or has overloads (or both).
Oh yeah, now I remember reading that caveat to ReturnType<>. Bummer.
This improvement still won't allow recursively enforcing type constraints on JSX children, right? The use case I'm thinking of is React's new Context API.
// good
render() {
let {Consumer, Provider} = React.createContext(undefined)
return <Provider value={{a: 1}}>
<Consumer>
{state => state.a}
</Consumer>
</Provider>
}
// bad
render() {
let {Consumer, Provider} = React.createContext(undefined)
return <Consumer>
{state =>
<div>
<Provider value={{a: 1}}>
{state.a} // runtime error
</Provider>
</div>
}
</Consumer>
}This has been an issue for so long. When will we get a fix for this?
My issue is similar to those described in the thread with typings children of ButtonGroup to only take Button.
My issue is that i have RenderProp components that enhance elements however they break when passed a Component instance instead of a native JSX.Element and there is no way to type against it currently.
a Component instance I.E <ButtonGroup/> is inherently different to say a native jsx element <div/> they have different properties and different behaviors and we need a way to type them accordingly.
- type on is a function type on native is a string.
- refs and event bindings behave differently when passed to one or the other.
- ReactDOM behaves differently when passed one or the other.
- refs when applied behave differently consider
ref.getBoundingClientRect();being called on a ref of a component instance vs a native jsx element.
I heard rumors this would be fixed with typescript 2.8, and then 2.9 but still nothing.
see comments for https://stackoverflow.com/questions/49269743/protect-against-react-instances
with references to github.com/Microsoft/TypeScript/issues/21699
Just to clarify, I understand that this issue is expected to allow us to define, for example, something like flowtype's https://flow.org/en/docs/react/children/#toc-only-allowing-a-specific-element-type-as-children
//โฆ from flowtype's documentation
type Props = {
children: React.ChildrenArray<React.Element<typeof TabBarIOSItem>>,
};
Am I right?
This feature could be a really big deal to use TypeScript in our Project (made with JS + React, looking for type-check it), because we have a lof of JSX components that we would like to type-check its children elements, and currently it is not posible to do with TypeScript.
Yes, technically. You can already define such a type in TS, it just isn't terribly useful since every JSXExpression is erased to the base JSXElement.
Related: #14789
Does this fix the following problem?
let foo = React.createElement(Foo)
foo = <Foo />
/* [ts]
Type 'Element' is not assignable to type 'ComponentElement<{}, Foo>'.
Types of property 'type' are incompatible.
Type 'string | ComponentClass<any, any> | StatelessComponent<any>' is not assignable to type 'ComponentClass<{}, any>'.
Type 'string' is not assignable to type 'ComponentClass<{}, any>'. */If not, does the above problem have a fix in the works?
I expect <Foo /> to return a React.ComponentElement<Foo['props'], Foo> value, not a JSX.Element value. At least when I'm using "jsx": "react" in tsconfig.json.
Maybe I can work around this by editing/overriding the exports of @types/react?
@aleclarson Yes, this would be the change which would hopefully allow that.
@weswigham Awesome!
I actually started work on this today, sorry ๐
Is it still a high priority for you? ๐
edit: Oh, looks like it's in-progress still. Can't wait!
Yeah, we got a lot of the way where with the new LibraryManagedAttributes entrypoint we added for react last release; but that just fixes up the props, the return type we have yet to manage.
Yeah, unfortunately right now when returning children you need to cast them to as JSX.Element | null ๐ข
@Kovensky or self-recursive array, or string, or number, I believe, as of react 16.
You can return them, you just have to cast them to as JSX.Element | null in current typescript, possibly with an as any in the middle. This is because TS had a hardcoded internal type for function components. I think TS@next fixes this but I haven't checked.
What I did when I encountered this issue was to just create ambient declaration react.d.ts and I am using only that everywhere. If it gets fixed someday, I can just change these to point to the real thing.
declare type ReactNode = string | number | React.ReactElement<any> | null
declare type ChildrenNode = ReactNode | ReactNode[]Btw, with hooks it won't be needed as much because render prop won't be that cool anymore ๐
What I did when I encountered this issue was to just create ambient declaration
react.d.tsand I am using only that everywhere. If it gets fixed someday, I can just change these to point to the real thing.declare type ReactNode = string | number | React.ReactElement<any> | null declare type ChildrenNode = ReactNode | ReactNode[]Btw, with hooks it won't be needed as much because render prop won't be that cool anymore
I still need it for a component that returns its previous or current children and animate them; it also use hooks (like usePrevious)
I didn't get how your custom type declaration for react helps this issue?
Well, it helps for a several reasons:
- There is no
undefinedas in the original so I don't get runtime issue from that - Since the
anyis neatly tucked in that file, I can useno-anyrule just fine - It's ambient, so I don't need to worry about importing it all the time :P
Had no issues with this so far.
@weswigham Do you have any status update on this?
To be able to do a real fix for this we'd probably need JSX.Element to be generic instead of an any-widened ReactElement.
Speaking of which, I should see what happens if I change the return type of FunctionComponent in 3.2.
Weighing in with support for this! I maintain jsx-pragmatic which is currently using Flow (and uses jsx in about the most dynamic way possible), tried to start converting it to TS today and ran into this. Definitely happy to play around with any beta builds and help test it out when there's something going here. ๐
Related: Flow just announced AbstractComponent, which is necessary to correctly support a lot of newer React features, at least in Flow-land - https://medium.com/flow-type/supporting-react-forwardref-and-beyond-f8dd88f35544
Definitely would love to see this fixed for our project. ๐บ
I've been looking for a way to strongly type React children in TS for years now. Is this something that's on the roadmap at all?
Well, there's a PR open with an implementation - #29818. It's really just a question of weather we think it's likely to be abused to too easily create programs which take an exponential time to check. (You'd think not, since it's no worse than nesting generic call expressions, and yet people don't nest call expressions 15 levels deep but do nest JSX tags that deep).
thanks @weswigham. Seems tricky. I routinely encounter very deeply nested JSX tags in real world code, so I could see real issues there. Would it make sense to be an opt-in feature?
people don't nest call expressions 15 levels deep but do nest JSX tags that deep
Yes, people do, believe it or not. You see it quite often in tree-building DSLs like hyperscript where the DSL is based in JS rather than transpiling to it. They don't usually hit 15, but 5-10 is pretty common and I've seen people get rather close to 15. In my experience, people nest hyperscript a little more deeply than they do with JSX because of the added conciseness of using h("div.foo", h("div.bar", ...)) instead of <div className="foo"><div className="bar">...</div></div> with React or <div class="foo"><div class="bar">...</div></div> with most other libraries. You'll also have similar concerns with Vue's render function which, although it lacks the dynamic selector support, is otherwise pretty similar to normal hyperscript DSLs. Other tree-building DSLs have similar constraints like babel-types, where in my experience, the typical call nesting depth is about 1.5x as deep as it is with typical JSX (1-3 in AST node building is like 1-2 in JSX, and 10-15 is like 5-10 in JSX).
As far as I understand, this issue was in TS 2.8 milestone. So now TS 3.6 is coming, but this wasn't fixed yet? I also can't find this in Roadmap, is there any work on this problem? Cause I'm still unable to return string from Functional Component and wrapping everything in React.Fragment gets annoying.
I'm not sure the issue your describing is correct. I think the typing is wrong on this.
If you check https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react/index.d.ts#L496 you can see that it accepts ReactElement | null but it should probably be ReactElement | Array<ReactElement> | string | number | null that are the supported elements to return from a functional component. Not sure why this is happening there, might be worth investigate
@felipeleusin @777PolarFox777 I already tried to a fix way back, but there was never a conclusion what to do about it...
Also worth noting, that this issue has a PR #29818 which is requesting folks test it on their codebases to get a sense of the performance implications - you can help that PR get merged by giving it a test run and posting the results in the PR ๐
I'm hitting the same roadblock: in a React web app, I also created code in separate files to emit plain XML documents with JSX and used the @jsx pragma. That works fine for the code emit, but the syntax checker is completely off and throws errors, for instance TS2345.
For now I can work around with an any cast like the following, but that's far from ideal.
/** @jsx Xml.createElement */
Xml.renderDocument(
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
</worksheet> as any)In addition to that, I had to add [xml: string]: any; to the JSX.IntrinsicElements interface, I could not find any other workaround for the IntrinsicElements errors.
@avonwyss in your case, you should just define Xml.JSX.Element - we support looking up custom nonglobal jsx namespaces for custom pragmas. This issue is mostly concerned with the ability for each tag, within a single jsx context, to have its own kind of return type (so you can distinguish different kinds of child tags).
First of all, thank you for all the awesome work on TypeScript. I love working with it and I hope it continues to improve.
I've looked at this problem again very recently as well for my library typed-jsx. It provides basic tools for working with JSX and creating JSX toolkits. https://github.com/gfmio/typed-jsx
The two JSX factory functions jsx and data that this package exposes are strongly typed, i.e. if I invoke the JSX factory directly, the type checker can infer the generic type parameters of the results automatically and thus guarantee type safety.
In contrast, when using JSX syntax, all of this information is lost - and unnecessarily so.
One of the examples I use in the code is to use the data jsx factory function to construct an object hierarchy representing file objects.
/* @jsx data */
import { data } from 'typed-jsx';
enum FileType {
File = 'file',
Directory = 'directory',
SymbolicLink = 'symlink',
}
interface Directory {
type: FileType.Directory;
name: string;
children: Array<File | Directory | SymbolicLink>;
}
const Directory = (props: Omit<Directory, 'type'>) => ({ type: FileType.Directory, ...props });
interface File {
type: FileType.File;
name: string;
data: Buffer | string;
}
const File = (props: Omit<File, 'type'>) => ({ type: FileType.File, ...props });
interface SymbolicLink {
type: FileType.SymbolicLink;
name: string;
target: string;
}
const SymbolicLink = (props: Omit<SymbolicLink, 'type'>) => ({ type: FileType.SymbolicLink, ...props });
const files = (
<Directory name="root">
<Directory name="sub">
<File name="file1" data="..." />
</Directory>
<Directory name="sub2">
<File name="file2" data="..." />
<SymbolicLink name="file3" target="../sub/file1" />
</Directory>
</Directory>
);
console.log(JSON.stringify(files, undefined, 2));
// Will print:
//
// {
// "type": "directory",
// "name": "root",
// "children": [
// {
// "type": "directory",
// "name": "sub",
// "children": [
// {
// "type": "file",
// "name": "file1",
// "data": "..."
// }
// ]
// },
// {
// "type": "directory",
// "name": "sub2",
// "children": [
// {
// "type": "file",
// "name": "file2",
// "data": "..."
// },
// {
// "type": "symlink",
// "name": "file3",
// "target": "../sub/file1"
// }
// ]
// }
// ]
// }Writing this using JSX feels natural and it's terse, especially compared to actually writing out the function definitions using normal syntax.
In my current set-up, the type data.JSX.JSXElement is any, so I cannot perform adequate type checking of the children of Directory, even though this would be perfectly possible when invoking the equivalent data() calls directly.
Another, more advanced use case for this would be using JSX for describing services registrations for an IOC framework, where nested services define error boundaries, component lifecycle events correspond to service lifecycle events and components can define configurable service registration patterns.
JSX is nothing but syntactic sugar for invoking a factory function. Hence, I think, it can and should be treated that way and not like a special black box, as it currently is.
JSX is nothing but syntactic sugar for invoking a factory function. Hence, I think, it can and should be treated that way and not like a special black box, as it currently is.
It's a little more complicated than that. For a couple examples:
- Inferno compiles it down to much more verbose and specialized calls, rather than just
Inferno.createElement. While you could useInferno.createElement, it's not advised. - babel-plugin-transform-incremental-dom reads them entirely opaquely.
And of course, there's JSX-oriented Babel plugins, too, that assign more meaning to JSX than simply being functions.
My ideal would just be this:
namespace JSX {
interface NativeType {
// Just repeated for each appropriate tag
[P in Tags]: (attrs: Attrs, ...children: Child[]) => Result
}
interface ComponentType {
(tag: Component, attrs: Attrs, ...children: Child[]) => Result
}
}That's all you really need for this, and that is sufficient to cover even those two libraries' use cases.
@isiahmeadows That's an interesting point. Can you please explain in more detail?
I just had a look at the inferno docs, but from what I could tell, you're still just calling a JSX factory method which returns some kind of object.
Similarly, the linked babel JSX plugin adds a number of helpers, but those are all private and shouldn't be exposed. It also doesn't matter what side effects are performed during the factory call since it doesn't affect the return type of the JSX expression. Even the library you linked will have a deterministic return type for the JSX expression, namely what is returned by elementClose.
My point is that even if the actual output of the JSX transformer is not a single function call, the accessible return value of the JSX is still a single value. So we could always wrap whatever output the JSX transformer generates in an anonymous function and the return type of that function would be the return value of the JSX element.
Additionally, when considering an expression like <Component attr={0} attr2="abc"/>, then I'm only interested in the return type of this and no matter your transformer, this return type will (hopefully) always be statically defined, so even if the output is not a function call, we can treat it as such at the type level.
Are there any other use cases you're envisioning that I haven't considered and that couldn't be expressed in this way?
I just had a look at the inferno docs, but from what I could tell, you're still just calling a JSX factory method which returns some kind of object.
Inferno does compile it to standard function calls, but it compiles it down to createVnode, createComponentVnode, and createTextVnode from inferno. It's not one-to-one text to string, attributes to object, and so on. There is an inferno-compat module that provides a React-compatible Inferno.createElement.
My point is that even if the actual output of the JSX transformer is not a single function call, the accessible return value of the JSX is still a single value. So we could always wrap whatever output the JSX transformer generates in an anonymous function and the return type of that function would be the return value of the JSX element.
I wasn't disputing that - here's what I said:
My ideal would just be this:
namespace JSX { interface NativeType { // Just repeated for each appropriate tag [P in Tags]: (attrs: Attrs, ...children: Child[]) => Result } interface ComponentType { (tag: Component, attrs: Attrs, ...children: Child[]) => Result } }That's all you really need for this, and that is sufficient to cover even those two libraries' use cases.
I was just stating it was a little more complicated than a simple desugaring at least at the runtime level. Typings can still be more or less that, obviously, as they're not literal proofs of a given implementation.
I havenโt read the whole thread (sorry) but I hope whatever solution we come up with would allow a polymorphic createElement type and polymorphic JSX.Element types which would allow us to make e.g. context providers/consumers and potentially hooks type safe; with some type wizardry and lightweight runtime wrappers or with monadic structures.
That would create the perfect typesafe runtime system for UIs.
After seeing Reactโs concurrent rendering friendly data fetching prototypes, I feel like that will become even more important.
@anilanar Sorry, but your princess is in another castle!
Available tags and contexts are not typeable with this language feature. They are effects, something like a second return type from a function, but computed as interection of all effects at function call sites. Specifically, covariant effects (unlike checked exceptions, which are contravariant). Once I tried to add support for them to TypeScript, but unfortunately hit a bug that is currently considered intended behavior. (Well, best luck to TS team working around that bug without fixing it!)
Given Hejlsberg's stance on checked exceptions, I wouldn't expect this issue to go any further, unless magic happens and we get algebraic effects in some ES2025, which will force TS to implement an effect system into the language. Most likely, with a little bit of working around intended behavior, it would be possible to use that same effect system for general effects (even in userland).
If you'd like to play a bit with effects, you might take a look on Koka. It's another JS-compiled programming language from Microsoft, but it didn't yet suffer from explosive growth and unavoidable pile-up of intended behavior.
@polkovnikov-ph Perhaps I phrased it wrong. What I'm requesting is somewhat orthogonal, if not irrelevant to what you are descring as the source of the problem. I request JSX to be 100% syntactic sugar that is transformed to a function call of our choice. Let's say, it's h:
// my weird JSX
/** pragma:jsx h */
const h = (tag: unknown, props: unknown, children: number[]): number => 1 + children.reduce((a, b) => a + b);
const myElem: number = <div><div /><div /></div>
// converted before typecheck to:
const myElem = h('div', {}, [
h('div', {}, []),
h('div', {}, []),
]);
// Everything typechecks, and works.
expect(myElem).toEqual(3);Then we can talk about whether TS is capable of describing such effects or not.
@anilanar Note: React.createElement is React.createElement(tagName, attrs, ...children), not React.createElement(tagName, attrs, children). And while React doesn't make a distinction internally between the two, other frameworks (like Mithril) may. (Part of why I feel it should be typed as either a type alias or callable interface, not directly linked to a specific type.)
@isiahmeadows what does the react/babel jsx transform do? I've always thought it to only be the latter, which would mean that is in fact the canonical signature, even if React.createElement happens to support a variadic form.
@ljharb It does return variadic arguments. Our m is intentionally JSX-compatible (and JSX setup instructions are in our docs), so I know this first-hand very well.
Based on the above mentions of this issue, several other issues across react are waiting for resolution on this. Is this on the roadmap to be resolved? Any updates that can be provided for the many folks watching?
Would implementing this allow users to restrict React children to specific component types? It seems this is something you can currently do in Flow but not Typescript (https://flow.org/en/docs/react/children/).
You already can in TypeScript by likewise changing the type of the accepted children property - that's been a thing for years IIRC.
@isiahmeadows But all elements have the same type JSX.Element regardless of which component they represent; which is one of the things we are trying to fix here.
That's not enforced by TS - you could do interface YourProps { children: symbol } for all TS cares, as long as the framework allows it within its JSX factory. And I can tell you from experience that React's type defs do allow you to do it.
The issue at hand (and what this issue is actually about) is with non-traditional component types that aren't even loosely React-compatible, not anything to do with what you can pass via the children attribute. Couple examples at a high level:
- Mithril.js:
type ComponentType<P, C> = Component<P, C> | ((vnode: Vnode<P, C>) => Component<P, C>) | (new (vnode: Vnode<P, C>) => Component<P, C>)wherePare props andCare children (tracked separately), used asm(Comp, props: P, ...children: C) - Cycle.js:
type ComponentType<Props> = (sources: Sources<Props>) => {DOM: Stream<JSX.Element>}, used asComp(sources: Sources<Props>)
@isiahmeadows From TS docs:
The JSX result type
By default the result of a JSX expression is typed as any. You can customize the type by specifying the JSX.Element interface. However, it is not possible to retrieve type information about the element, attributes or children of the JSX from this interface. It is a black box.
In flow, JSX.Element isn't a black box, it takes a generic parameter for component type.
Example from flow docs:
type Props = {
children: React.ChildrenArray<React.Element<typeof MyComponent>>
}And my proposal was to make TS respect the types of jsxFactory. You can see an exaggerated example above in which JSX element expressions are typed as number.
Whatever solutions come out of this, please TS team don't make it React-specific in any way.
The ecosystem is always morphing, and React is merely but an alternative to other libraries and frameworks that also have JSX syntax that work in unique ways, and we don't know if React will always be the most used component system around.
I don't know how it should be implemented, but it should be possible to have the following be possible at some point (for example using the variant of JSX currently found in the Solid.js component system):
const div = <div>...</div>
console.log(div instanceof HTMLDivElement)
function MyComponent() {
return <p>...</p>
}
function App() {
return <>
<span>...</span>
{div}
<MyComponent />
</>
}
const comp = MyComponent()
console.log(comp instanceof HTMLParagraphElement)
const app = App()
console.log(app instanceof Array)
console.log(app[0] instanceof HTMLSpanElement)
console.log(app[1] instanceof HTMLDivElement)
console.log(app[2] instanceof HTMLParagraphElement)where the type of div is correctly inferred to be HTMLDivElement, comp is inferred to be HTMLParagraphElement, and app inferred to be the tuple [HTMLSpanElement, HTMLDivElement, HTMLParagraphElement].
Here is a live example of this actual runtime behavior in Solid.js:
https://playground.solidjs.com/?hash=-1086708363&version=1.2.2
It would be helpful if we could understand why #29818 was closed. Perf problems? No testing?
Alternatively, instead of looking up the createElement type, we could look up the type of a new JSX.ElementType type (defaulting to the old behavior if JSX.ElementType isn't declared). @types/react could then define it (roughly) as type ElementType = string | FunctionComponent | ClassComponent. This seems like a simpler implementation with less risk of causing severe performance problems and would be fully backwards compatible (assuming nobody declares JSX.ElementType at the moment).
The main problem is () => string | any[] | number not being a valid JSX function component. But we'll now face more problems if async server components (which would return a Promise) land.
It would be helpful if we could understand why #29818 was closed. Perf problems?
Indeed, some codebases had their check times go up nearly five-fold:
See also
@RyanCavanaugh I wonder if there's some middle ground that could be achieved by moving all the heavy lifting to the component instead of the factory. This should hopefully result in the node count overhead growing based on the number of components, rather than also the number of JSX nodes (outside generics, where those types would need inspected anyways).
- The
JSX.Intrinsic*interfaces could be retained as-is. JSX.Elementcould be changed to be looked up as a simple named type upon checking a JSX<Element />node. Minor perf impact, but it's likely to already be in the cache (and if not, it could be reasonably hard coded for), so it should be fairly minimal.JSX.ElementClasscould be retained as-is.JSX.ElementFunctionwould need defined to verify function types more generally than with just one parameter.- Stencil's functional components require more than one parameter.
- Alternatively, the
ParametersForElementFunctioncould just accept whatever the function's initial parameter is, if that use case is deemed too niche.
JSX.ParametersForElementClass<Instance extends JSX.ElementClass>andJSX.ParametersForElementFunction<Params extends Parameters<JSX.ElementFunction>>could be used to direct TypeScript to the effective attributes and children for JSX type checking purposes, whenJSX.ElementAttributesPropertyandJSX.ElementChildrenAttributearen't sufficient.neverwould render the element invalid, as{}is not assignable tonever.- For React and related, the first could be
{attributes: Instance["props"], children: Instance["props"]["children"]}, and the second{attributes: Params[0], children: Params[0]["children"]}. - For Mithril, the first could be defined as
Instance extends {view(vnode: {attrs: infer A, children: infer C}): any} ? {attributes: A, children: C} : never, and the second{attributes: Params[0]["attrs"], children: Params[0]["children"]}. - For Stencil's functional components, the second could be defined as
{attributes: Params[0], children: Params[1]} - If needed, these two
JSX.ParametersFor*types could themselves be interfaces, at the cost of requiring some duplication betweenattributesandchildren. - Note: this kind of model might break type inference for generics, but I can't remember if generics can be inferred in the face of such a transform.
- For React and related, the first could be
Keep in mind that if Typescript doesn't tackle this, the future is any: DefinitelyTyped/DefinitelyTyped#62876
This issue is a blocker to DefinitelyTyped/DefinitelyTyped#18912
Is there any progress or plan on this? Our team wishes to take the advantage of tsx syntax to simplify object creation. We customize all JSX related factory and can get all type checking worked except <foo></foo> could only have type JSX.Element. Such a behaviour erase the type information indeed and force all result of <></> to be extended from a base class.
@weswigham why did you reopen this?
Because only part of the use-case was addressed. The type of components/intrinsics as permitted by the factory will be controllable by the JSX.ElementType in TypeScript 5.1; but TypeScript 5.1 will not actually consult the factory.
i'm struggling to follow this thread a bit (apologies). would someone mind clarifying if this will address the following so that TS complains in this scenario?
const Parent: React.FC<{ children: React.ReactElement<{ testing: string }> }> = () => null;
const Child: React.FC<React.PropsWithChildren> = () => null;
() => <Parent><Child>test</Child></Parent>;i saw it had been closed but this doesn't work when i had seen previous comments suggesting it would. was it reopened because of this?
Simply passing the necessary type to JSX.Element (as mentioned here) as a type parameter would cover most use cases.
The only thing that might be added is a check for whether the return type of the factory function always satisfies JSX.Element.
First of all, thank you for all the awesome work on TypeScript. I love working with it and I hope it continues to improve.
I've looked at this problem again very recently as well for my library
typed-jsx. It provides basic tools for working with JSX and creating JSX toolkits. https://github.com/gfmio/typed-jsxThe two JSX factory functions
jsxanddatathat this package exposes are strongly typed, i.e. if I invoke the JSX factory directly, the type checker can infer the generic type parameters of the results automatically and thus guarantee type safety.In contrast, when using JSX syntax, all of this information is lost - and unnecessarily so.
One of the examples I use in the code is to use the
datajsx factory function to construct an object hierarchy representing file objects./* @jsx data */ import { data } from 'typed-jsx'; enum FileType { File = 'file', Directory = 'directory', SymbolicLink = 'symlink', } interface Directory { type: FileType.Directory; name: string; children: Array<File | Directory | SymbolicLink>; } const Directory = (props: Omit<Directory, 'type'>) => ({ type: FileType.Directory, ...props }); interface File { type: FileType.File; name: string; data: Buffer | string; } const File = (props: Omit<File, 'type'>) => ({ type: FileType.File, ...props }); interface SymbolicLink { type: FileType.SymbolicLink; name: string; target: string; } const SymbolicLink = (props: Omit<SymbolicLink, 'type'>) => ({ type: FileType.SymbolicLink, ...props }); const files = ( <Directory name="root"> <Directory name="sub"> <File name="file1" data="..." /> </Directory> <Directory name="sub2"> <File name="file2" data="..." /> <SymbolicLink name="file3" target="../sub/file1" /> </Directory> </Directory> ); console.log(JSON.stringify(files, undefined, 2)); // Will print: // // { // "type": "directory", // "name": "root", // "children": [ // { // "type": "directory", // "name": "sub", // "children": [ // { // "type": "file", // "name": "file1", // "data": "..." // } // ] // }, // { // "type": "directory", // "name": "sub2", // "children": [ // { // "type": "file", // "name": "file2", // "data": "..." // }, // { // "type": "symlink", // "name": "file3", // "target": "../sub/file1" // } // ] // } // ] // }Writing this using JSX feels natural and it's terse, especially compared to actually writing out the function definitions using normal syntax.
In my current set-up, the type
data.JSX.JSXElementisany, so I cannot perform adequate type checking of the children ofDirectory, even though this would be perfectly possible when invoking the equivalentdata()calls directly.Another, more advanced use case for this would be using JSX for describing services registrations for an IOC framework, where nested services define error boundaries, component lifecycle events correspond to service lifecycle events and components can define configurable service registration patterns.
JSX is nothing but syntactic sugar for invoking a factory function. Hence, I think, it can and should be treated that way and not like a special black box, as it currently is.
You're so on point. JSX.Element in TypeScript being a black box just surprises me. How did they come to the conclusion that throwing away all of that info is the best thing to do? As you said, using the JSX syntax is just syntactic sugar over calling the factory directly, so it should be treated as that. I really hope they do something about it.
I'm confused about why the need for the whole global JSX.Element* apparatus in the first place. The jsx transform is simple enough, so why not just do it and then use the types of the factory functions to both constrain the types of arguments and assign the type of the return value?
Or is that the performance issue? Too many overloads?
@weswigham Thanks for the explanation, although I must admit I don't quite understand what a "typespace way to resolve overloads means." Do you have a concrete example of a scenario where the TypeScript type system is not expressive enough?
@weswigham Thanks for the explanation, although I must admit I don't quite understand what a "typespace way to resolve overloads means." Do you have a concrete example of a scenario where the TypeScript type system is not expressive enough?
Think function overloading, but as applied to types. Kind of like this:
type Foo<T extends number> = One
type Foo<T extends string> = TwoObviously a bit contrived, but you get the point. And overloading crossed with declaration merging is where the real power is - you can't shim around that using conditional types.
@dead-claudia I think I kinda see what you're getting at (kinda) although I'm not getting how this connects to the jsx factory function. In my particular case, I'm using JSX to express an in-memory HTML syntax tree; Nothing stateful, just an AST describing describing an HTML document.
Here's a simple system with only div and button
interface HTMLButtonAST {/* ... */}
interface HTMLButtonAttrs {
autofocus?: boolean;
disabled?: boolean;
}
interface HTMLDivAST {/* ... */}
interface HTMLDivAttrs {/* ... */}
interface HTMLFragment {/* ... */}
type HTMLAST = HTMLButtonAST | HTMLDivAST | HTMLFragment;
interface HTMLASTConstructor<TAttrs, TAST extends HTMLAST> {
(attrs?: TAttrs): TAST;
}
// here are the overloads for the jsx factory.
function jsx(tagname: "button", attrs?: HTMLButtonAttrs): HTMLButtonAST;
function jsx(tagname: "div", attrs?: HTMLDivAttrs): HTMLDivAST;
function jsx<TAttrs, TAST extends HTMLAST>(
ctor: HTMLASTConstructor<TAttrs, TAST>,
attrs: TAttrs,
): TAST;If I use the jsx() function directly, the type checking of the arguments and the inference of the return type are all correct:
let one: HTMLButtonAST = jsx("button", { autofocus: true, disabled: false });
let two: HTMLButtonAST = jsx(MyButton, true);
//@ts-expect-error string is not assignable to boolean
jsx(MyButton, "false");
//@ts-expect-error Argument of type '{ nosuchattr: string; }' is not assignable to parameter of type 'HTMLButtonAttrs'
jsx("button", { nosuchattr: "wat" });
function MyButton(disabled?: boolean) {
return jsx("button", { disabled });
}I know I'm missing something, but given that the JSX is just syntactic sugar for invoking this function, I'm wondering why this is not enough to infer the type of a JSX expression.
Is there a plan for this? Is jsx ever going to use the factory function signature?
There are so many use-cases that are now blocked by this design. Why not simply treat it as if it's the actual function call, just with the different syntax? Can't we simply do a dumb transform internally to the function call and let the normal TS inference work? Why is it more complicated than this?
Is there a specific problem why TypeScript doesn't resolve the actual JSX factory call and instead returns a "black box" type?
I mean, having a strongly typed XML-like syntax in the language can open up a wide variety of uses for JSX, rather than just UI frameworks. And it doesn't seem to break anything in current use cases of JSX, since we can reference JSX.Element when it's presented in the JSX namespace instead of the factory return type.
Also, it seems rather odd that typescript has this "JSX result is a black box" design, since jsx is just a fancy syntax for function calls :\
Thanks!
Also, it seems rather odd that typescript has this "JSX result is a black box" design, since jsx is just a fancy syntax for function calls :\
It's not just a fancy syntax for function calls. JSX has a rather loose spec
It's intended to be used by various preprocessors (transpilers) to transform these tokens into standard ECMAScript
One of the projects that use it differently is SolidJS where it works like this
const element: HTMLDivElement = <div/>It's not just a fancy syntax for function calls. JSX has a rather loose spec
But it does just transform down into function calls at transpile time, right? So I would have the same question as @stagas:
Can't we simply do a dumb transform internally to the function call and let the normal TS inference work? Why is it more complicated than this?
One of the projects that use it differently is SolidJS where it works like this
const element: HTMLDivElement = <div/>
Which would still work if this issue were resolved and the Typescript compiler would respect the jsx factory function for both attributes and return type.
function jsxFactory<T extends keyof HTMLElementTagNameMap>(
type: T,
props: Partial<HTMLElementTagNameMap[T]>,
...children: HTMLElement[]
): HTMLElementTagNameMap[T](A very crude example ๐ )
I agree with others that the current JSX implementation is rather... unnecessarily complex. Having all these specific 'ghost' JSX interfaces you can define to satisfy help the type checker, instead of just basing everything off of the jsx factory's signature.
@bluepnume SolidJS doesn't compile JSX to simple function calls. They currently compile them to IIFEs for the DOM, and a _$ssr(template, ...) call (almost a template tag but not quite) for server-side rendering. Edit: And the return types for server and client rendering are very different, with it returning an element when compiled for client-side rendering and a {t: string} object when compiled for SSR. The precise type I believe is actually an implementation detail and should be an opaque type.
Sure, but I don't think it's that outlandish for typescript to default to simple function calls given that that is already how the tsc transpiler handles them. For example this is the tsc output:
/** @jsx jsxFactory */
const foo = ( <div /> )"use strict";
/** @jsx jsxFactory */
const foo = (jsxFactory("div", null));-- it feels like this should be the default for the type system too, with some way to override for frameworks like SolidJS that don't follow this convention?
Yes, that's called a syntactic sugar and I think that's the most future proof way to do it in my opinion but that may not be the most practical one considering the kind of baggage TS carries due to backward compatibility in terms of both functionality and performance.
If a target library (like SolidJS) needs to use a black box, they are free to do so.
@bluepnume TS also supports outputting JSX literally, mainly for React Native, but Solid requires this as well.
Is this the issue that tracks the ability to make this finally happen?
declare namespace JSX {
type IntrinsicElementTypes = {
button: HTMLButtonElement,
a: HTMLAnchorElement,
ul: HTMLUListElement,
'': DocumentFragment,
}
}
const b = <button />; // now inferred as HTMLButtonElement
const a = <a />; // now inferred as HTMLAnchorElement
const f = <Foo />; // now inferred as HTMLUListElement
const d = <></>; // now inferred as DocumentFragment
function Foo() /* return type inferred as HTMLUListElement */ {
return <Bar />;
}
function Bar() /* return type inferred as HTMLUListElement */ {
return <ul />;
}Because I really need that feature since I use vanillajsx daily.
I'd be glad to contribute towards this feature full time until it's done.
Also, IntrinsicElementTypes would be backwards-compatible, as its default definition would be:
type IntrinsicElementTypes = {
[tag: string]: JSX.Element,
'': JSX.Fragment,
}@sdegutis Yes, this is the issue. No, correct implementation breaks TS type checker due to a classic unfixable mistake in language design. Here are similar issues in Swift and C# (yes, Anders did it twice).
This should be pinned to the top of this thread to prevent others from wasting their time here.
@sdegutis Yes, this is the issue. No, correct implementation breaks TS type checker due to a classic unfixable mistake in language design. Here are similar issues in Swift and C# (yes, Anders did it twice).
This should be pinned to the top of this thread to prevent others from wasting their time here.
Assuming @reverofevil is correct. He's certainly confident, but I've met too many overconfident software engineers who turned out to be wrong. I'd need to see some kind of proof, or someone with reknowned credibility confirming this feature is "unfixable".
@sdegutis I came to this issue for a very similar reason that you did. I just wanted jsx to function as an alternate syntax for creating JavaScript values.
You may want to check out hastx It seems like it is very similar in spirit to vanillajsx. It just creates a value that represents an HTML AST. The main reason it exists is to be an extension to the UnifiedJS ecosystem which includes things like MDX and friends.
@sdegutis Oh, I'd be very happy to be proven wrong! But just for the context:
TS has something called "context-sensitive resolution", the thing that allows you omit function argument types when that function is assigned into a typed variable
type F = (x: number) => void
const f: F = x => console.log(x + 1); // x: numberBecause of this, for code with two nested calls to overloaded functions, TS has to first iterate over overloads of the outer function, so that expected argument type can be applied as context to resolve inner function
interface F {
(x: true): number;
(x: false): string;
(x: boolean): number | string;
}
interface G {
(f: (x: 1) => void): true;
(f: (x: 2) => void): false;
(f: (x: 1 | 2) => void): boolean;
}
declare const f: F;
declare const g: G;
const r: number = f(g(x => console.log(x))); // x: 1Here TS should resolve the type for f in context of number, iterates over its overloads, and for each of those overloads does another context-sensitive resolution for g with 2 overloads. So we got 2^2 = 4 function signature checking operations.
In case of JSX there a thousands of overloads in createElement, and tens of nested calls, which results in thousands^tens ~ 10^30 function signature checking operations per component. I'm not an expert in TS codebase, and didn't research it properly, but take a look at the comment of TS developer who did.
Performance is terrible. Like beyond bad. This makes every jsx tag tree in your program into a series of nested generic context sensitive function calls (and most jsx apps have a lot of nested tags!). That's just about the worst-case scenario for the type checker.
I don't see any solutions here except for "do not check types in a way TS does it". Textbook implementation would use (linear-time) unification instead of context-dependent resolution, and would constrain ad-hoc polymorphism so that every signature is an instance of a more generic type to check call sites against. Unfortunately, first thing is incompatible with pretty much all novel types added to TS (especially conditional types), and second would require changing all the syntax and semantics of overloading.
But again, who am I to tell about type theory textbooks. I'm pretty sure developers knew what they're doing, and made intentionally exponential type checker, so that we have time for a coffee while the code compiles.
@sdegutis I came to this issue for a very similar reason that you did. I just wanted jsx to function as an alternate syntax for creating JavaScript values.
You may want to check out hastx It seems like it is very similar in spirit to vanillajsx. It just creates a value that represents an HTML AST. The main reason it exists is to be an extension to the UnifiedJS ecosystem which includes things like MDX and friends.
@cowboyd Yeah it'd be great if JSX expressions could be typed based on usage, it would be a win for the whole community, if only this other guy who thinks he knows more than Anders turns out to be wrong, or if he's right but tsc deprecates function overloading (probably unlikely tho I'd be okay with it personally since I never use it and consider it bad practice anyway).
Funny enough, I think my original JSX Monarch that I've since lost the code to and forgot how to do was actually written as part of a proprietary closed-source alternative and/or extension to MDX for a client a few years ago. It's interesting to see how the community keeps dividing with oh so similar ideas that are almost compatible. I wish I could use hastx in vanillajsx to solve that. But then I'd still need a compiler, which defeats the "vanilla" in vanillajsx, or I'd need to add a helper function that transforms the AST to DOM objects, which is what I originally had in imlib before I landed on what I have now. (Technically I'm already using a compiler, but that's only because there's no vanilla JSX in ES spec yet. Ideally JSX would be native syntactic sugar for something, and I've been brainstorming what that might be for a few years, but I'm not really part of any community, so it's effectively just me wondering out loud, which is pointless and ineffective.)
Hmm..... reading over your thoughts, it sounds like what is needed is jsx literals
The closest thing I can find is this discussion which seems to have petered out a couple of years ago. Just glancing over it though, it seems like it's too complicated. What would be wrong with a straightforward literal syntax for javascript values?
<a href="foo">bar</a> // => { tag: "a", attrs: { href: "foo" }, content: "bar" }"Components" are not special, they are just a function in tag position.
const Foo = () => {}
<Foo x=1 y=0/> // => { tag: Foo, attrs: { x: 1, y: 0 } }Because there is no factory function at all, there are no magical overloads, the structure of the value is known at compile-time and so any functions accepting or returning said values can be typed according to existing rules.
