microsoft / TypeScript

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

Alternative syntax for type assertions to allow XML-like syntax extensions #296

Closed thorn0 closed 9 years ago

thorn0 commented 10 years ago

The current syntax for type assertions prevented the React team from adding TypeScript support to JSX. While E4X was a failure, XML-like syntax extensions is a good idea, which might make it into some next ECMAScript version. If it happens, it will be impossible to incorporate that syntax into TypeScript because of type assertions.

(ported from http://typescript.codeplex.com/workitem/2608)

CyrusNajmabadi commented 10 years ago

"it will be impossible to incorporate that syntax into TypeScript because of type assertions."

This is not the case. As i mentioned in the original thread:

Hey Guys,

TS developer here (and guy who wrote the current 1.0 parser). I haven't read through the entire thread. However, from looking at a few examples of what you're asking for, this seems like it would be very easy to add in a fork of the typescript parser, if one were so willing.

The algorithm would be pretty darn simple. First, you'd prescan the file, looking for XML close tag candidates. i.e. you'd have a regexp that would match strings like (allowing for spaces between things). With that, you'd have a table mapping potential end tags to locations in the file.

You'd then need to augment the scanner and parser. The parser would be updated such that if it hit a potential opentag, it would need to speculatively parse ahead to see if this was actually XML or just a cast. The end-tag table would be helpful here as it would allow you to even decide if it was worthwhile to do that speculative parse. i.e. if you saw and had never seen in your doc, then there would be no point speculatively parsing. On the other hand, if you had and did have a tag later on, then it would be worth going forward.

If you never find the end tag, then you rewind parsing, and go ahead parsing the entity as a type assertion expression.

This approach is very similar to the one we already need to take today to deal with the ambiguity between type assertion expressions and generic arrow function expressions. Speculative lookahead is necessary (and already done here), so it's no stretch to augment it to support XML. The only little trick here is to get the end tags to ensure you don't speculatively parse every time, most of the time when it's not going to be fruitful.

danquirk commented 10 years ago

Cyrus covered some of the issues here. Changing the syntax for type assertions would be a pretty serious breaking change. If we have to do so for some reason in the future (like a future EcmaScript version) then I think we cross that bridge when we get there.

thorn0 commented 10 years ago

I didn't propose to change the syntax. I asked for adding an alternative one. It's not a breaking change.

CyrusNajmabadi commented 9 years ago

@thorn0 I think the subtle issue here is that while you are proposing an alternative syntax for type-assertions, you seem to be implicitly stating that the existing type-assertion syntax would then be used for the xml-like syntax extensions. That part is what could be a significant breaking change.

That said, i still think this is very possible to do. However, i don't think it currently has enough interest to warrant the work right now. If you can show that ther'es enough interest in the community for this, that could change. Thanks!

koistya commented 9 years ago
If you can show that ther'es enough interest in the community for this, that could change.
CyrusNajmabadi

There is about 100k monthly downloads of Rect.js library from the npm registry. Even if only 10% of this audience would willing to use TypeScript with React+JSX that looks big enough, IMHO. But, as of now, all this audience is steadily switching from TypeScript to Flow.

DanielRosenwasser commented 9 years ago

For the record, you can easily subsume this functionality with template strings/tagged template strings, which will be available in 1.4.

borekb commented 9 years ago

Now that TypeScript is a first-class citizen in Angular (2.0), it would be nice if React got some love too. There are ways to integrate them today but they are clunky, not very IDE-friendly, etc. I'd personally love if I could combine the two technologies seamlessly.

Lenne231 commented 9 years ago

:+1: definetly a must have! I would love to have type safety in my whole application, even in the views

thorn0 commented 9 years ago

@CyrusNajmabadi

I think the subtle issue here is that while you are proposing an alternative syntax for type-assertions, you seem to be implicitly stating that the existing type-assertion syntax would then be used for the xml-like syntax extensions. That part is what could be a significant breaking change

It's not the case. Right, the existing syntax wouldn't be used in the files containing JSX expressions. However, the new syntax is all that is needed from TypeScript here. It's not a breaking change at all. The JSX expressions would be processed by the JSX processor, not by TypeScript. The TS compiler would get normal TypeScript code as its input, without any extensions. How is this a breaking change?

The React team would've been glad to add the support for the TS constructions to JSX, but the current syntax for type assumptions just makes it too complicated.

borekb commented 9 years ago

This is a noob question but how comes that Flow doesn't have problem with angle brackets + generics + JSX and TypeScript does?

thorn0 commented 9 years ago

There is no problems with generics, only with type assertions. Flow doesn't use angle brackets for them.

borekb commented 9 years ago

Ah ok.

Ciantic commented 9 years ago

Is the problem here with JSX where there is angle brackets? Couldn't JSX just simply have escaping possibility:

var a = 
    <SomeComponent>
    {
        if (someFunc\<generic\>(param)) {
            <NotGeneric>
        }
    }
    </SomeComponent>

Edit or in case of type-assertions:

var a = 
    <SomeComponent>
    {
        if ((\<SomeType\> a).test) {
            <NotGeneric>
        }
    }
    </SomeComponent>

Either way, I think JSX transformer should have escaping.

DanielRosenwasser commented 9 years ago

It's definitely worth mentioning on this issue that @fdecampredon has a fork jsx-typescript

Ciantic commented 9 years ago

Forking TypeScript :-1: is like expecting this one guy to use all his remaining free time to merge.

This thread does not intend to add JSX support to TypeScript but instead have possiblity to write TypeScript inside JSX syntax. And I think this problem should be taken to JSX transformer, and perhaps not discussed in TypeScript issue list at all.

fdecampredon commented 9 years ago

@Ciantic for what it worth I intend to maintain my jsx fork since I need it for my own projects.

fdecampredon commented 9 years ago

Also note that with the technique that @CyrusNajmabadi described I was able to support type assertion and jsx syntax, so I don't think there is a need for a new type assertion syntax.

borekb commented 9 years ago

I will always have much harder time convincing my bosses to use a TypeScript clone rather than TypeScript itself. And with React Native, it's even more possible that React / JSX will be an important technology to support in the near future.

fdecampredon commented 9 years ago

@borekb while I understand your point what do you expect ? I mean actually jsx syntax is a syntactic sugar created for React and completely dependent of React, for example:

<div />

Is compiled down to :

React.createElement('div', null);

It's not the purpose of typescript to support every dsl language created by every js framework out there, and I would be seriously surprised that the typescript team decide to support something like that directly.

If you don't want to use a fork you still have few options :

var h = React.DOM;
h.div(null, h.span({className: 'something'}))// instead of  <div><span class="somehing" /></div>
var h = React.createElement;
h('div', null, h('span', {className: 'something'},)); // instead of <div><span class="somehing" /></div>
borekb commented 9 years ago

@fdecampredon

It's not the purpose of typescript to support every dsl language created by every js framework out there

Agree, but I'd argue that React is not just "another JS framework" and I believe it (incl. JSX) should get some consideration from the TypeScript team. All your alternatives make it possible to combine TS + React which is good and I know about them but the convenience and/or toolability is simply not there. There is a reason why FB introduced JSX and there is a reason why it's not in string templates, comments etc.

NoelAbrahams commented 9 years ago

but I'd argue that React is not just "another JS framework"

It looks and smells rather like a JS framework. For it not to be a framework, it should be indispensable. I believe React deals with the specific problem of binding a JavaScript model to the DOM. The alternatives are Knockout JS and Angular JS.

fdecampredon commented 9 years ago

Agree, but I'd argue that React is not just "another JS framework" and I believe it (incl. JSX) should get some consideration from the TypeScript team. All your alternatives make it possible to combine TS + React which is good and I know about them but the convenience and/or toolability is simply not there. There is a reason why FB introduced JSX and there is a reason why it's not in string templates, comments etc.

That's why I made jsx-typescript :D

borekb commented 9 years ago

Don't get me wrong, I'm glad your fork exists. However, for real-world projects, there are other important questions to be answered like does it have support in Visual Studio? In WebStorm / IntelliJ IDEA? How quickly is the fork updated for new releases of TypeScript? Etc. All these questions wouldn't exist if TS supported JSX out of the box which is why I think it would be valuable.

fdecampredon commented 9 years ago

Yup I understand @borek, in the meantimes I can answer your question:

fdecampredon commented 9 years ago

Also note that jsx is not the only painful point of React/TS integration, see https://github.com/Microsoft/TypeScript/issues/1926 and https://github.com/Microsoft/TypeScript/issues/1947, jsx-typescript resolve that by creating special cases with type inference and jsx-element, I doubt the official ts compiler could make this kind of choice.

borekb commented 9 years ago

Thanks for shedding some more light on it, @fdecampredon.

thorn0 commented 9 years ago

It's not the purpose of typescript to support every dsl language created by every js framework out there

What if TS and its language services provided a generic extension point for plug-in code preprocessors? Preprocessors would generate TS source files (or AST) and something like source maps. The language services would consume those maps and this way enable the support for any kind of DSL in VS and the other IDEs that use the language services. Sounds too utopian, right?

mindplay-dk commented 9 years ago

I mean actually jsx syntax is a syntactic sugar created for React and completely dependent of React

Does it have to be?

That's actually not what I thought was being proposed at all.

What I was hoping for was something along the lines of e.g.:

var who = 'World';
var doc =
    <foo>
        <bar baz="bam" hello=who/>
    </foo>;

Being interpreted as something along the lines of:

var who = 'World';
var doc = foo({
    children: [bar({
        attrs: {
            baz: "bam",
            hello: who
        }
    })]}
);

With functions looking something like this:

class Bar {}

function bar(params: { attrs: { baz: string; hello: string } }) {
    return new Bar();
}

function foo(params: { children:Bar[] }) {
    // ...
}

The names in params would need to be standardized as e.g. attrs, children, etc. and aren't really important due to structural sub-typing, which would enable type-safety even on attribute values and children.

I don't know that it would work like this exactly, that's just a quick sketch, but I was expecting basically tags to be interpreted as function-names, attributes allowing both for string literals and expressions and getting passed as a hash - and either parents receiving their children as arguments, or children receiving the parent as argument, I'm not sure which is better or more flexible..

But so, basically, general-purpose syntactic sugar for function calls where parent/child relationships are important.

You could build any object/data structure you want in this way - doesn't have to be specific to any framework or even to XML or HTML, you could use it for anything.

mindplay-dk commented 9 years ago

Also note, no plug-ins required - this is not a framework, it's just syntactic sugar, and IDE support via the language service would be possible out-of-the-box.

NoelAbrahams commented 9 years ago

But so, basically, general-purpose syntactic sugar for function calls where parent/child relationships are important.

Wouldn't that require following some sort of convention? (I mean to the extent the attributes and element names are translated into functions.) And if we are to follow non-Ecmascript conventions then that hints at TypeScript supporting one framework or the other.

You could build any object/data structure you want in this way - doesn't have to be specific to any framework or even to XML or HTML, you could use it for anything.

Isn't that what JSON is for?

fdecampredon commented 9 years ago

@paulvanbrenk I'm sorry to ask that, but I've seen that you added a branch called 'jsxSupport' with basic jsx tests, is it planned to support jsx ? if yes I could perhaps do a pr ?

billti commented 9 years ago

Hi @fdecampredon. We’re in early experiments to see what it would take to add JSX support, and if we could get a solid implementation without impacting performance or compiler complexity too greatly. Nothing committed yet, but we are aware of the value for TypeScript users this would have. Your existing work in this space is awesome, and we’d be more than happy to have you help us in any way.

mindplay-dk commented 9 years ago

Wouldn't that require following some sort of convention? (I mean to the extent the attributes and element names are translated into functions.)

You would need a convention only for the names used for the object that gets passed as the argument to the functions, e.g. children and attrs - element names are 1:1 with function names, so no convention there, and attribute-names are 1:1 with property-names in attrs, so no convention needed there either.

And if we are to follow non-Ecmascript conventions then that hints at TypeScript supporting one framework or the other.

This is just alternative syntax (sugar) making complex function call structures easier to read. Anyone can implement such functions for any framework, or for that matter, simple functions that don't require any framework. For example, a simple set of functions could be written to return DOM elements and apply attribute-values as HTML attributes.

You could build any object/data structure you want in this way

Isn't that what JSON is for?

JSON is a data structure, it doesn't build data structures - it doesn't actively "do" anything, it's a data format.

What I'm proposing here is not a template syntax - if you implement functions that perform template engine work, you could of course implement templates, but the point is, it's really just syntax for nested function-calls, so you could implement just about anything. What comes out of an XML-like expression literal isn't a document object model of some sort (as in JSX) which needs further processing by being passed through a function, which then has to parse that document model using code and probably a state-machine.

Really, all you get with JSX (as far as my understanding) is an XML parser, which means you have a very weak coupling to the programming language itself - you still then have to write an actual document parser, which has to do run-time work to make sense of element names and attributes etc... so you get no static coupling to any real language features, most importantly type-checking, and deciding which functions to invoke or what to do with the document becomes entirely run-time.

I think what I'm proposing is much simpler, thinner, and more directly coupled to the language - the idea is not simply to build data structures (which we can already do with object literals) but to make complex function-call trees more legible.

A simple example to get you thinking about this:

function a(params: { attrs: { href: string }; text: string }) {
    return `<a href="${params.attrs.href}">${params.text}</a>`;
}

var a = <a href="http://github.com">GitHub</a>;

The interpretation of the XML-like initialization is literally:

var a = a({attrs:{href:"http://github.com"}, text:"GitHub"});

So the compiler (and language service) can type-check at design-time, e.g. checking the presence and types (and structural sub-types) of the arguments. It can also infer the return-type of the top-level call to a(), which in this case is string, but the syntax works for building document models too:

function a(params: { attrs: { href: string;  }; text: string }) {
    var e = new HTMLAnchorElement();
    e.setAttribute('href', params.attrs.href);
    e.textContent = params.text;
    return e;
}

var a = <a href="http://github.com">GitHub</a>;

The point is, this syntax works for building anything, not just for building documents, and it leverages type-checking, return-types, inference, etc. - rather than just embedding documents and type-checking XML syntax, this type-checks arguments, return-types, child element types, etc. which seems much simpler and more powerful to me. You don't need a run-time document parser, and you're leveraging all the strengths of the language.

One more simple example to get you thinking about this - using optional attributes and closures:

function a(params: { attrs: { href: string; onclick?: { (ev: Event):any } }; text: string }) {
    var e = new HTMLAnchorElement();
    e.setAttribute('href', params.attrs.href);
    e.textContent = params.text;
    if (params.attrs.onclick) {
        e.addEventListener("click", params.attrs.onclick);
    }
    return e;
}

var a = <a href="http://github.com" onclick=(ev) => alert('leaving!')>GitHub</a>;

The last initialization is literally interpreted as:

var a = a({ attrs: { href:"http://github.com", onclick: (ev) => alert('leaving!')}, text:"GitHub" });

So here you get type-checking for the optional onclick callback.

Try to imagine the safe, expressive DSLs you could build with type-checking on both function-calls, attributes and children - as opposed to a simple embedded XML syntax, where all of these type-checks need to happen at run-time in a document parser.

I know this isn't where Facebook is heading with JSX, but is that the right direction? Is it even an interesting feature? We have object and array literals for document-like data structures already, providing the same basic type-safety, though, actually, probably more type-safety. I don't see JSX really integrating with the language, so much as co-existing inside it - to me, that's not very interesting or very powerful.

Perhaps in plain JS this is more relevant, since you don't have any type-checking or inference to leverage in the first place - but in TS, I think we can do better?

paulvanbrenk commented 9 years ago

@mindplay-dk I'm not sure what you're proposing.

Do you propose a library which can generate Dom Elements, using functions like the 'a' function you define in your post? As you show in your post, this is something you can create right now, no changes needed... Or do you propose a transpiler, which takes the 'jsx' syntax and converts it a method call using your 'a' function.

Our current thinking is that there will be (and are) many transpilers from jsx -> js, so the biggest gain for TypeScript is to support 'jsx' constructs in a 'tsx' file. Thus adding a step to the existing workflow, tsx -> jsx -> js, while giving users the freedom to chose their jsx transpiler.

Make sense?

thorn0 commented 9 years ago

@mindplay-dk

I know this isn't where Facebook is heading with JSX

I think you're wrong. I can't see any difference between JSX and what you're describing. Except that you're using different properties of the only parameter for attrs and children whereas Facebook just uses different parameters for them. But that doesn't count. Have a look at the JSX spec: https://facebook.github.io/jsx/

fdecampredon commented 9 years ago

@paulvanbrek the compiler will not transpile jsx constructs, but if so how will jsx element be typed ?

paulvanbrenk commented 9 years ago

@fdecampredon that's still an open discussion, Flow types them as 'any'.

fdecampredon commented 9 years ago

@paulvanbrenk While the type of an element itself is not so important, having types for attributes is invaluable, and flow has special case for react element if I remember correctly. Also in that scenario we would loose any kind of completion, and I don't think that a lot of jsx compiler support sourcemap double sourcemap. For all this reason I'm a little afraid of the added value of the proposition.

mhegazy commented 9 years ago

@fdecampredon can you share an example where you would use attribute types.

As for typing the expression, I believe we have 3 options:

1. type an xml expression as any

pros:

cons:

2. type it as {}

pros:

cons:

3. type it as React.ReactElement/React.DOMElement

pros:

cons:

robertknight commented 9 years ago

A variation might be to have a way to tag XML literals, similar to template string tagging or decorators which would pass the resulting object to a function that interprets the attributes and types the result.

eg.

import jsx = require('jsx');
return @jsx <div foo=bar>;

In the "jsx" module:

interface JSXLiteral {
  [index: string]: <union of types that an attribute value can have>;
  children: (JSXLiteral | string)[];
}

function jsx(obj: JSXLiteral): React.DOMElement { ... }

I'm wary of complicating the language for this though, and the knock-on complexity it could add to all the tooling related to TypeScript, eg. syntax highlighting, linting, etc.

mindplay-dk commented 9 years ago

@thorn0

I know this isn't where Facebook is heading with JSX

I think you're wrong. I can't see any difference between JSX and what you're describing.

For one, everything passes through a global function React.createElement(), which couples it to a framework, and would mean no static type-checking for elements/attributes (?)

It also seems like the result is always an object model, at least as intermediary? Of course you could pass the resulting object model to something that generates strings, but that would mean a full object model always need to be constructed first, then processed separately. No reason this shouldn't work for strings or any other return type, as far as I can figure.

thorn0 commented 9 years ago

@mhegazy In the previous versions of React, JSX files contained so called JSX pragma: /** @jsx React.DOM */. Then the React team decided to get rid of it. Something like that pragma can be introduced in TypeScript to define the semantics for JSX syntax constructs and to leave a possibility to change it without breaking existing code.

Ciantic commented 9 years ago

@thorn0 I think what @mindplay-dk meant is that don't bake this support with thinking about React too much.

Consider this <Something prop={5} /> is transpiled to React.createElement(Something, {prop: 5}, ... obviously this is not a good idea when using TypeScript, since it can't type check the properties. To get it fully typed it should be something like Something({prop : 5}, ...

thorn0 commented 9 years ago

@Ciantic @mindplay-dk Ah, okay. Last time I looked at React's JSX transformation it transformed <div/> to React.DOM.div(). It's not longer the case.

Ciantic commented 9 years ago

@thorn0 yeah, it's not longer the case. And I can't understand why? Facebook has this parellel Flow typing effort going on, how are they going to make property checks typed now? I get the feeling it's going to change more as the Flow becomes more mature.

One can try the latest JSX compiler in here: https://facebook.github.io/react/jsx-compiler.html

jbrantly commented 9 years ago

@Ciantic Flow is very tightly integrated with React and JSX. For instance, Flow looks at calls to React.createClass along with propTypes to do type checking of props in JSX.

fdecampredon commented 9 years ago

@mhegazy With React JSX :

<MyComp prop={true} />;
<div id="hello" />;

is transformed to :

React.createElement(MyComp, {prop : true });
React.createElement('div', {id : 'hello' });

With React.createElement signature being:

createElement<P>(type: { new(p: P): React.Component<P> }, props: P): ReactElement<P>
createElement(type: string, props: HTMLAttribute | SVGAttribute): ReactDOMElement

Having type-check on P properties is really useful. With jsx-typescript I had to add special rule in call resolution for JSXElement so type was inferred only from the first argument (and P was typed correctly).

By the way, I'm working on a cleaned version of jsx-typescript based on the master and I'll submit a PR just to feed the discussion.

fdecampredon commented 9 years ago

@mhegazy I think a good solution for typing could be to let the user set the factory function for jsx element with directive/options, for example :

/// <jsx-factory function="React.createElement" />

class MyComponent {
....
  render() {
     <div /> // typed as React.createElement('div', null); not transpiled
  }
...
}
...

Or :

tsc --jsxFacotry=React.createElement

This way anybody could use JSX for his own framework and it would be typed correctly

Note: I think it's really important to have to have type inferred only from the tag in case of JSX Element, without that the typing is near to be useless for React (and I think for any other framework that would like to use JSX)

Ciantic commented 9 years ago

@fdecampredon Is this valid TypeScript definition?

createElement<P>(type: { new(p: P): React.Component<P> }, props: P): ReactElement<P>

If that is all it takes to make properties type checked it should be commited to here: https://github.com/borisyankov/DefinitelyTyped/blob/master/react/react.d.ts I tried it, it doesn't throw any weird errors but couldn't get the property type checks to work, at least without jsx-typescript fork.

gulbanana commented 9 years ago

as a happy user of jsx-typescript, this is an encouraging discussion- so much of javascript is views, and typechecking for them is of course super valuable. I'm excited to see the possibility of react+ts gaining first party support