microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
99.99k stars 12.37k forks source link

Proposal: JSX.ElementType #13890

Open antanas-arvasevicius opened 7 years ago

antanas-arvasevicius commented 7 years ago

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: https://github.com/Microsoft/TypeScript/issues/13746

RyanCavanaugh commented 7 years ago

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.

antanas-arvasevicius commented 7 years ago

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};
    }
}
antanas-arvasevicius commented 7 years ago

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.

RyanCavanaugh commented 7 years ago

@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

antanas-arvasevicius commented 7 years ago

Hi, I'll make a small public demo today by copying some of our project files where you could test that behavior. Here is UST+02:00 timezone so I think you'll get it tommorow. You could write some questions also or a behavior you would expect to see in a demo.

antanas-arvasevicius commented 7 years ago

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 https://github.com/Microsoft/TypeScript/pull/13891 explicit casting could be removed.

Any questions are welcome.

antanas-arvasevicius commented 7 years ago

Hello, @RyanCavanaugh , could you please give any feedback for this issue? Are there are any plans to support this and how is it going?

RyanCavanaugh commented 7 years ago

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:

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.

antanas-arvasevicius commented 7 years ago

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.

morrisallison commented 7 years ago

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. https://github.com/Microsoft/TypeScript/issues/4195#issuecomment-298189688

ceymard commented 7 years ago

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...

antanas-arvasevicius commented 6 years ago

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.

TotooriaHyperion commented 6 years ago

Related: #23457

morrisallison commented 5 years ago

Related: #21699

dead-claudia commented 5 years ago

Related: https://github.com/Microsoft/TypeScript/issues/13260

bsunderhus commented 2 months ago

Any updates on this? or https://github.com/microsoft/TypeScript/issues/14729