Proposal: JSX.ElementType
antanas-arvasevicius opened this issue ยท 16 comments
Hello,
There are many GUI frameworks (ExtJs, SmartClient, OpenUI5) which do not works in React way and they could be easily be integrated with JSX/TSX if their JSX expressions would return correct "element type" e.g.
const listGrid = <ListGrid/> ; // should be type of ListGrid or isc.IListgrid, not JSX.Element
My proposal would be to add new special type into global JSX namespace - ElementTypeProperty.
JSX.ElementTypeProperty
Given an element instance type, we need to produce a type that will be return type of that JSX element. We call this the element type.
The interface JSX.ElementTypeProperty defines this process.
It may have 0 properties, in which case element instance type will be element type.
Or 1 property, in which case the element type will be the type of that property type of element instance type.
Note: Intrinsic lookup is not affected by ElementTypeProperty.
Related: #13746
I think this is a reasonable idea. I recall some other suggestions to this effect and it seems like this behavior is going to be relatively common among non-Reactlike frameworks.
My main concern is that it seems really plausible that some framework would define ListGrid to be a reference to an object containing a create method or other factorylike pattern where the name of the element just gives you something that needs to go through overload resolution. We might mitigate that with one of the open proposals to support typeof producing the return type of a function call, though.
Hi,
you can look at our implementation of Isomorphic SmartClient's JSX support.
Shortly:
All SmartClient components has a create method in it, e.g. isc.ListGrid.create, but the problem is that "children" property will differ, in ListGrid it would be fields, in Canvas would be children, in VLayout would be members. So we've took approach by defining new classes which has different factorylike public static create(...) methods which produces GUI object. Then added SmartClientJSX .createElement(...) which just delegates create task to these static create factories.
I think other frameworks would also need that manual wiring too, but if they are consistent maybe something like typescript interface ElementTypeProperty { create(); } would work?
What's your thoughts on this?
smartclient.jsx.d.ts:
declare module JSX {
export interface IntrinsicElements { }
interface Element extends isc.ICanvas { }
interface ElementClass extends Component<any, any> { }
interface HtmlElementInstance extends ElementClass { }
interface ElementAttributesProperty { __props; }
interface ElementTypeProperty { __elementType; }
}smartclient.gui.ts: (Note: all isc.* definitions are written in separate .d.ts file)
namespace SmartClientJSX {
export function createElement<P extends Component<T, M>, T, M>(elementClass: {new (...args: any[]): P}, props: T, ...children: Component<any, any>[]):M {
return (<any>elementClass).create(props !== null ? props : {}, children);
}
}
abstract class Component<T, M> { private __props: T; private __elementType: M;
static create(params: any[], children) {}
}
// property 'children' will be children objects.
class Canvas extends Component<isc.ICanvasOptions, isc.ICanvas> {
static create(params, children) {
return isc.Canvas.create({
...<any>params,
children: params.children ? [...params.children, ...children] : children
});
}
}
// property 'members' will be children
class VLayout extends Component<isc.IVLayoutOptions, isc.IVLayout> {
static create(params, children) {
return isc.VLayout.create({...<any>params, members: children});
}
}
// property 'fields' will be children
class ListGrid extends Component<isc.IListGridOptions, isc.IListGrid> {
static create(params, children) {
return isc.ListGrid.create({
...<any>params,
fields: params.fields ? [...params.fields, ...children] : children
});
}
}
// no children allowed, you can see that ListGridField for ListGrid is just plain object.
class ListGridField extends Component<isc.IListGridFieldOptions, isc.IListGridField> {
static create(params) {
return {...params};
}
}
// property 'fields' will be children
// allow to pass fields in attribute "fields" and concat children as fields.
class DynamicForm extends Component<isc.IDynamicFormOptions, isc.IDynamicForm> {
static create(params, children) {
return isc.DynamicForm.create({
...<any>params,
fields: params.fields ? [...params.fields, ...children] : children
});
}
}
// FormFieldItem for DynamicForm.fields is just plain object
class FormFieldItem extends Component<isc.IDynamicFormFieldOptions, isc.IDynamicFormField> {
static create(params) {
return {...params};
}
}
// property 'tabs' is children
class TabSet extends Component<isc.ITabSetOptions, isc.ITabSet> {
static create(params, children) {
return isc.TabSet.create({...<any>params, tabs: params.tabs ? [...params.tabs, ...children] : children});
}
}
class Tab extends Component<isc.ITabOptions, isc.ITab> {
static create(params) {
return {...params};
}
}Hi, we are thinking to use forked version of tc internally till this feature will be released to public if this feature will be accepted in future. I know that you have more priority work, but is it possible to know how long should it take till this request will be discussed and be accepted or rejected? Thanks.
@antanas-arvasevicius we talked about this for a while yesterday and had a bunch of questions. Is there a repo that uses this that I could look at to better understand the behavior? Otherwise I can post the Qs here
Hi, Ryan,
I've just made some demo where you can explore the workings of JSX ElementType:
https://github.com/antanas-arvasevicius/elementtype-demo
just git clone && npm install && tsc && node server.js
demo will be on http://localhost:9999/
You can see that now we must explicitly cast each :
/src/page/demo/DemoPageLayout.tsx
/src/layout/MainLayout.tsx
/src/page/demo/Dialog.tsx
Using patched compiler in PR #13891 explicit casting could be removed.
Any questions are welcome.
Hello, @RyanCavanaugh , could you please give any feedback for this issue? Are there are any plans to support this and how is it going?
I have a lot of open questions on this with regards to how general-purpose it is. Specifically, the problem of whether the element type appears on the instance side or static side of the constructor function - there seem to be requests from people on both sides of this. The major contingents are:
- Frameworks that work like WPF/XAML and create instances+set properties on the actual type specified (the element type is the class instance type)
- Frameworks that work like React and do opaque deferred instantiation (the element type is a fixed JSX.ElementType)
- People who want to encode additional semantics into their React-like frameworks, e.g. they want a
<Foo><bar/></Foo>construct to somehow enforce that only<bar />or<baz />elements are passed as children toFooeven though the deferred instatiation might otherwise imply an opaque element type - Frameworks that do immediate or deferred instatiation of some other type (yours as far as I can tell) ?
- Open question is whether elements here should be assignable based on the constructor function type, or the instance type
My current thinking is that a long-term solution would be changing the JSX element type resolver to be similar to resolving the return type of a function. This opens up basically arbitrary possibilities (via generic type inference) but is quite a bit more complex.
Thank you for your detailed response. Yes, seems complicated problem :)
For specifically our case, we are "integrating" open source GUI framework into TSX coding style and we cannot have "class instance type" because original GUI objects doesn't work well and we need to have some "wrappers" which instantiates these original GUI objects. So yes we need an ability to somehow define that if you would write: const layout = <HLayout/> you would get an instance of "some other type" which is as in my proposed variant would be the type of " __elementType" property of the HLayout class defined by interface ElementTypeProperty { __elementType; } similiar workings as with attributes (interface ElementAttributesProperty { __props; })
And if there is a need to have an actual type specified it could be defined as ``interface ElementTypeProperty { }` (empty interface)
Is this solution doesn't cover full cases? (1, 2, 4 I think is covered, for 3 - is this question in this jsx element scope?)
About some "type resolver function" and using "return type" as a result would solve all problems, but it will be difficult to "parse/execute" this during type checking? If it wouldn't the ElementAttributesProperty could be implemented in the same way.
My current thinking is that a long-term solution would be changing the JSX element type resolver to be similar to resolving the return type of a function. This opens up basically arbitrary possibilities (via generic type inference) but is quite a bit more complex.
Great, this would solve the issue I mentioned here. #4195 (comment)
I have a use case for a library I'm writing where I'm basically using tsx to generate DOM nodes directly (with extra sauce on top).
I would like a lot to be able to specify that <div/> returns a HTMLDivElement or an HTMLInputElement instead of just a plain Node as I am doing right now with some ugly casts.
Couldn't the whole tsx transform simply apply the function call and type check it ? My guess is that it would also solve the whole generics issue since the type inferer works well with them -- directly calling the factory function without using tsx generally works fine...
Hi,
looks like this feature is still not on your (public) roadmap list, is there are any technical issues to implement it? ("JSX element type resolver to be similar to resolving the return type of a function.")
If it's technically doable then I can try to contribute and implement it, just some guidelines on how to approach it would be nice to have.
Related: #23457
Related: #21699
Related: #13260
Any updates on this? or #14729