Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TypeScript support for JSX #759

Closed
thorn0 opened this issue Dec 31, 2013 · 83 comments
Closed

TypeScript support for JSX #759

thorn0 opened this issue Dec 31, 2013 · 83 comments

Comments

@thorn0
Copy link
Contributor

thorn0 commented Dec 31, 2013

Would be great if JSX recognized TypeScript constructs.

@syranide
Copy link
Contributor

syranide commented Jan 1, 2014

JSXTransformer uses fb-esprima which is a JSX-enhanced dialect of esprima, which is a complete JavaScript parser. So for JSX to support TypeScript would require extending or replacing esprima with a complete and compatible parser for TypeScript. Unless such a thing already exists, doing this would most likely require significant initial and ongoing effort.

Unless you are willing to put in that effort, it's unlikely that this is going to happen any time soon or at all in the foreseeable future. After all, JSX is only sugar for very similar looking and functionally equivalent JS code.

@thorn0
Copy link
Contributor Author

thorn0 commented Jan 1, 2014

Why does JSXTransformer need a JS parser at all? Why can't it just replace its XML-like constructs with JS code wherever it finds them (except string and comments)?

@sophiebits
Copy link
Collaborator

Contrived example:

<input value={this.props.text + "}"} />

The value shouldn't end at the first }; you need at least some JS parsing logic to understand that.

@syranide
Copy link
Contributor

syranide commented Jan 1, 2014

@spicyj Or just something as simple as "<tag />" should not be parsed by JSX.

@thorn0 You are probably right in that it isn't strictly necessary, but it aids in generating valid code, as we now actually makes sure that the JS parses before move on.

@sebmarkbage
Copy link
Collaborator

Technically you don't need a parser but you need a tokenizer.

Sweet.js macros operate on the token stream before it hits the parser. JSX almost works with Sweet.js except there are special rules for regular expressions. Closing tags doesn't work with the / token depending on context. The regexp resolution rules are hardcoded into the tokenizer. Sweet.js probably can't support our whitespace rules neither.

However, if we treat JSX as strictly part of a custom tokenization stream, we can do the transformation purely on the token level, just like Sweet.js. That should leave it compatible with any other language extension that doesn't need special tokenization rules. The resulting code would even be compatible with Sweet.js macros.

What do you think @jeffmo ?

@syranide
Copy link
Contributor

syranide commented Jan 1, 2014

@sebmarkbage As long as we assume that all opening brackets must have a corresponding closing bracket it should work technically. It will also have to assume that expressions, object notation, array notation, etc is compatible or the output will have to be configurable.

But we will lose the ability to detect and report syntax errors before we inject {expressions} into the output stream, so eventual errors in the output stream will likely be a lot less comprehensible as a result, I'm unsure about how bad they actually can become in the worst case.

There's no denying that there are advantages to operating on the token stream like this, I just fear that it will be at the expense of making JSX less user friendly and thus less appealing, JSX is just sugar so I would never sacrifice the convenience of really good error message with the convenience of something that looks like HTML. Hopefully I'm wrong and it's not bad at all, but I think it's a very fine line between JSX being a very nice optional feature and a nice toy.

JSX is really compelling to me, primarily because it's cleaner than the JavaScript equivalent without any immediate drawback (--watch makes it painless enough), but it still lacks features that vanilla JavaScript offers, which means that you have to revert to JavaScript from time to time, so I still stick to JavaScript, but I can see that I may switch to JSX in the future. But I would never if I end up with crappy error messages that plagues so many template frameworks. But that's my opinion.

@sebmarkbage
Copy link
Collaborator

Even if it was true that we can't get good enough error messages using a token-transform alone you can still have the parser provide context to the tokenizer.

If your parser can provide a compatible context , that's great, you get better error messages for free.

If it can't and you have to pipe the raw transformed strings, you still have the option to do that. If you want to use TypeScript or HipsterScript you can.

We can't add a convenient extension to the language and expect that nobody else will add other extensions. People will want them both. If you have to choose between TypeScript or JSX then JSX will certainly be seen as the toy.

Unless there is an ambiguity problem ofc. TypeScript may not be compatible for other reasons. That's why I like macros because they can be contextual extensions. An identifier can be treated differently depending on if it's a known React/JSX component or a Type/Class/Interface.

@thorn0
Copy link
Contributor Author

thorn0 commented Jan 2, 2014

TypeScript is similar to Sweet.js macros in that it's a superset of JS, just another convenient extension. However, yes, there might be incompatibilities as the type cast operator looks like this in TypeScript:

this.span = <HTMLSpanElement>document.createElement('span');

Oops...

@syranide
Copy link
Contributor

syranide commented Jan 2, 2014

@sebmarkbage You're more experienced in this area, but it seems like you would have always to explicitly extend the parser of the target language or use the standalone token-transform version, the likelihood of their and our parsers and tokenizers being compatible seems remote (unless esprima is the parser in the JavaScript world). Or am I missing something?

If that's the case, I haven't looked into the details, but it seems like a simple fix to just implement an optional parseProgram/parseExpression in fb-esprima which has no knowledge of the language features at all other than that brackets must be balanced for the sake of XJS expression containers. Token-transform for free really. The only issue is that string/comment syntax would have to be compatible, or the tokenizer would need to be configurable as well.

I could probably even put together a proof-of-concept if it's worth pursuing that path.

@jordwalke
Copy link
Contributor

Apologies for just chiming in without reading the discussion. I just thought I'd let you know the result of a previous discussion I had with someone who started adding JSX support into TypeScript. We found it was difficult to distinguish type parameters Array<Things> from JSX. My suggestion was to require that all JSX be wrapped in parens like (<Typeahead />). This makes it easier to parse, and most of the time we end up wrapping our JSX in parens anyways to guard against Automatic Semicolon Insertion, so people are used to it.

@syranide
Copy link
Contributor

syranide commented Jan 2, 2014

@thorn0 @jordwalke It seems like Array<Things> shouldn't be an issue to solve if you're extending the actual parser (but perhaps you're not), as the parser should not be in a state where it would consider JSX to be valid. However, <A>1</A> seems intuitively harder to solve as we cannot know if <A> is JSX or a type-cast... until we've found the (possible) closing </A>, but that does not seem reasonable.

It seems to me that your suggestion (<A>) has the same issue unless you explicitly disallow type-casts as the first instruction inside parens, which isn't perfect, but should very rarely be an issue. So it's a surprisingly neat distinction/solution to a major ambiguity.

@sebmarkbage
Copy link
Collaborator

I was worried about maintaining two versions. In theory you could make a tokenizer that can be a drop in replacement in an esprima based typescript parser. But that's certainly more difficult.

This could be a nice quick fix. You can always add special look ahead/behind rules. It doesn't have to be pure.

On Jan 2, 2014, at 7:14 AM, Andreas Svensson [email protected] wrote:

@thorn0 @jordwalke It seems like Array shouldn't be an issue to solve if you're extending the actual parser (but perhaps you're not), as the parser should not be in a state where it would consider JSX to be valid. However, 1 seems intuitively harder to solve as we cannot know if is JSX or a type-cast until we've found the closing but does not seem reasonable.

It seems to me that your suggestion has the same issue unless you're explicitly disallowing type-casts as the first instruction inside parens, which isn't perfect, but should very rarely be an issue. So it's a surprisingly neat distinction/solution to a major ambiguity.


Reply to this email directly or view it on GitHub.

@syranide
Copy link
Contributor

syranide commented Jan 3, 2014

@sebmarkbage I'm playing with a proof-of-concept and it was a simple as I had hoped, the major issue that I hadn't foreseen is that it's basically not possible unambiguously detect the initial tag, without the parser to provide context or without any additional starting token/constraint. ...<abc is that a comparison perhaps?

If a language supports a feature like <abc> then we're basically all out of luck unless we require an initial parenthesis as @jordwalke suggested. It could make certain edge-cases in existing scripts break though (x(<string>y())) and there would be no obvious no-op way of escaping it for the user, so it's not 100% safe with just parens. The benefit would be that it wouldn't really look out-of-place in any language as you can't really get away from the mathematical parenthesis in any language.

So I'm unsure how flexible/verbose we should make it, the major issue being that if we require an additional token for the starting tag, it has to be repeated for every branch of conditional expressions with tags.

@sebmarkbage
Copy link
Collaborator

This is already a problem in JavaScript with regular expressions. You can still implement JS highlighting without a proper parser because you can use look behind to disambiguate. It's not easy to enumerate all the potential cases you'll have to look for though. Hopefully they're bounded.

Sweet.js has a pretty good write up on this for regular expressions. Since those are allowed in similar places as JSX I figure the solution would be similar. https://github.com/mozilla/sweet.js/wiki/design

@sebmarkbage
Copy link
Collaborator

Of course the extended language could add features that you can't account for. So it may not always work. That's why it would be great to have at least a little bit of feedback from the parser. E.g. if a regular expression is allowed, then can we also assume that < is the start of a tag.

@syranide
Copy link
Contributor

syranide commented Jan 3, 2014

Yeah, so I took a step back and analyzed the problem properly. I see three ways of doing this:

  1. Look-ahead for <aaa>, <aaa /> and <aaa bbb=.
  2. Look-behind for any operator:ish character :=,[({+-*/, etc and look-ahead for <a.
  3. Both.

  1. The straight-forward approach, we assume only that none of those exact occurrences are allowed by the target language. <aaa> is directly conflicting with TypeScript and I see no way out of it without modifying our syntax and <aaa bbb= is conflicting with a hypothetical language that allows a non-comma-separated object notation, with = for assignment instead of the default :.
  2. We assume that the target language is based on operators, and we can easily test that we are reasonably inside of an expression, in a state where a value/identifier is expected. This would explicitly prevent support for wordy languages that would allow let A be <tag> (if there even is a worthwhile one for JavaScript, I doubt it). TypeScript sadly conflicts here too as <TypeCast>... is valid in the same contexts as <Tag> (even if we had their parser to provide context).
  3. Stricter, but TypeScript would still be an issue. So not sure if there's a point.

Unless I'm missing something, TypeScript can only be solved (with or without parser backup) by either modifying the syntax for the empty root tag (like <!tag>, <tag empty> or whatever) or using something like what @jordwalke suggested. However as I mentioned above, the parenthesis constraint doesn't actually solve the ambiguity. What you're really doing is disallowing type-casting as the first instruction inside of parenthesis, and replacing it with JSX-tags.

@sebmarkbage Your take? Do we need an alternate syntax for special-cases?

@sebmarkbage
Copy link
Collaborator

@vjeux I can't think of any case where generics are ambiguous with JSX. Because they're always preceded by an identifier which is not valid JSX. They're ambiguous with JavaScript though!

Type casting is a big issue though. The parenthesis doesn't help.

Most cases are actually not ambiguous:

(<foo>x) // cast
(<foo />x) // error, x is invalid after a component
(<foo />) // jsx
(<foo attr="">x) // error, missing closing tag
(<foo attr="">x</foo>) // jsx can be early determined by the attributes

The parsing code and error messages becomes really weird when you have an opening tag without attributes. You have to optimistically parse ahead a long way to find the matching closing tag which breaks the ambiguity.

(<foo>x + (y) + </foo>)

Even then it's ambiguous with regexps.

(<foo>x</foo>/+5)
// could mean:
(<foo>(x < new RegExp('foo>') + 5))
// or
(foo(null, x) / 5)

So, yea. The type cast syntax screws us up. I really want to make this work though.

@sophiebits
Copy link
Collaborator

Note that (<foo>x)</foo>) is valid JSX.

@sebmarkbage
Copy link
Collaborator

@spicyj It's not valid TypeScript though. Need to make it into a RegExp to make it ambiguous I think. Maybe you can think of another case?

@sophiebits
Copy link
Collaborator

Sorry, I thought you were implying that you could stop parsing at the close paren. You may be right that regexes are the only tricky part; perhaps we can solve that by requiring people to wrap regex literals in parens? I think JSLint might already warn about that. Still, it does sound like we may need arbitrary lookahead to disambiguate which sounds like a recipe for confusion.

(One other idea I mentioned to @syranide in IRC was requiring people to wrap each JSX expression in backticks, sort of like how we recommend JSX in CoffeeScript now. Obviously that's a bit of a pain but it easily removes any ambiguity.)

@sebmarkbage
Copy link
Collaborator

Ideally we would use

jsx`<foo>${x}</foo>`

To make it fully executable ES6. I think that JSX is always close to being more of a pain than it's worth. Compared to just invoking functions. Back ticks, as small as they seem, might be the final straw.

@sebmarkbage
Copy link
Collaborator

I guess back ticks would actually remove an ES6 feature unless we prefix with something. So it doesn't work without a prefix anyway.

@syranide
Copy link
Contributor

syranide commented Jan 9, 2014

@sebmarkbage We cannot reliably disambiguate (<foo>x)+(<foo></foo>) (in a real context). Sure with an infinite look-ahead we could possibly make it reliable enough to actually be useful, but any error messages would be complete dog poo as we would never be able to identify intent. Which in my eyes makes it useless.

I agree with backticks, while it would be a solution, it would remove an ES6 feature and it would be quite error-prone when dealing with nested conditionals. Although realistically one could likely just assume that any <> inside an expression is JSX and not a type-cast.

However, <aaa> is the only issue, the two other cases can be disambiguated, quite easily. So reasonably, we only need to introduce an (optional) syntax for <aaa>, but it is not trivial what it would look like and possibly not very neat either. The simplest being <aaa >. However, for all the different optional syntaxes I can come up with, none really feels natural or not weird to me.

@sebmarkbage
Copy link
Collaborator

Yes. I agree that infinite lookahead is not an ok solution. It's more of a thought experiment to see where it leads us.

I think that you're right that an alternative syntax for cast is the right way to go here. Particularly since this syntax is not really intuitive or common for the current use in TypeScript anyway. It would seem that making that change is possible.

Some ideas without really thinking it through... The simplest most intuitive to me is that you type the expression:

var x = { } : foo;

Another alternative would be to add a contextual keyword somehow:

var x = cast<foo> { };
var x = cast { } as foo;
...

@syranide
Copy link
Contributor

syranide commented Jan 9, 2014

@sebmarkbage The only thing I worry about effectively overriding TypeScript syntax is that this would be a solution only for TypeScript, and it would effectively mean that either you end up with two different type-cast syntaxes depending on whether the current file is run through the parser or not, or you risk breaking existing code if all files are run through it.

Also, depending on the replacement syntax for type-casts, you potentially have to extend the actual TypeScript parser as opposed to just having a language agnostic transform which a special-case for TypeScript-style type-casts. What are the current thoughts on extending the language parsers vs a language agnostic transform? It seems like having an agnostic transform would be preferable, and if people really are invested in TypeScript (or whatever) then nothing prevents them/us from implementing a "language native" solution down the road.

One potential idea, if my JSX namespaces PR is accepted and merged, would be to simply have the syntax be something like <.tag> or <:tag> and thus <.React.DOM.div> or <:React:DOM:div>, not the neatest syntax, but there's some logic to it at least. Perhaps an acceptable trade-off for languages where it conflicts?

<tag>
</tag>

vs

<.tag>
</tag>

@sebmarkbage
Copy link
Collaborator

@syranide There can always be conflicts and we have to solve them on a case by case basis. Namespaces could easily conflict with another language too.

You can also just use an external function if we kill the type assertion operator.

class Parent {}
class Child extends Parent {}

function cast<T>(obj : any) {
    return <T>obj;
}

var x : Parent = new Child();
var y = cast<Child>(x);

@syranide
Copy link
Contributor

@sebmarkbage Ah, interesting solution.

@fdecampredon
Copy link

There is other problems with typescript + React than just the JSX syntax.
TypeScript currently does not support mixins at all, (and won't before at least some months I would say).
So it's basically very hard to make it understand what is the result of React.createClass (and I doubt it will be ever possible to make it understand React mixins).
I've tried to work with React + typescript, and ended up wrapping a lot of React mechanism to be able to make TypeScript fully understand type of what I worked with.
Perhaps the real solution here would be to create a JSX to Typescript definition file compiler to integrate with typescript, and keep JSX for creating React components.

@syranide
Copy link
Contributor

@fdecampredon It seems like React intends to move to ES6 classes soonish, I'm assuming that would improve the situation?

@jbrantly
Copy link
Contributor

@fdecampredon I meant this: https://github.com/fdecampredon/jsx-typescript
Release may have been too strong a word. I meant "make available publicly".

@fdecampredon
Copy link

I finally decided myself to finish this work : https://github.com/fdecampredon/jsx-typescript/ is an alpha version, it is just working and need extra works. it's based on master branch of typescript.
I'll publish it to npm soon but if someone could help me by testing it a bit (especially in visual studio, since I'm on OSX) that would help, thanks!

@abergs
Copy link

abergs commented Feb 5, 2015

@fdecampredon Very cool. I will test it when I have some available time. 👍

@tedvanderveen
Copy link

Cool!!

@basarat
Copy link
Contributor

basarat commented Feb 5, 2015

Will definitely take it for a spin ❤️

@pspeter3
Copy link

pspeter3 commented Feb 5, 2015

Congratulations!

@jbrantly
Copy link
Contributor

jbrantly commented Feb 9, 2015

@fdecampredon Thanks for your continued contributions in this space! Wish you had released before React.js Conf, definitely would have mentioned it in my talk :(

I just updated ts-loader for webpack to support jsx-typescript, so if you're using TypeScript+JSX+webpack you may be interested.

@fdecampredon
Copy link

Thanks @jbrantly unfortunately I had too much work to do before react conf and since my company don't use typescript nor react I can only work on that topic when I have free time.
Thank you for ts-loader! I generally use browserify but I'm thinking about switching to webpack since the workflow with browserify is a bit painful.

@pspeter3
Copy link

pspeter3 commented Feb 9, 2015

I'm using browserify + gulp for incremental TypeScript compilation and can write about that if it would be helpful. Need to check out ts-loader

@nathggns
Copy link

nathggns commented Feb 9, 2015

Something to flag up: I've written a proposal for another use of the < and > characters within the TypeScript language. While it is only used in places where defining generics can already be done, I just want to make sure there are no conflicts.

It's over at microsoft/TypeScript#1985

@fdecampredon
Copy link

@nathggns I don't think there will be any problem, place where a generic can be defined was really not prone to conflict with JSX, the hardest part was about type assertion, which has been resolved with lookahead and close-tags map like @CyrusNajmabadi described, the only compromise I had to accept is to forbid the usage of the char } in jsx text (you have to use entities) to disambiguate cases like:

<foo>{<foo>something}</foo>

After they might be still case I have not think about if anyone find one please report an issue.

@f10et
Copy link

f10et commented Mar 11, 2015

@pspeter3 have any config to share ?

@pspeter3
Copy link

@scboffspring For gulp and browserify?

@f10et
Copy link

f10et commented Mar 16, 2015

@pspeter3 Yes please

@petilon
Copy link

petilon commented Mar 23, 2015

The discussion so far mostly talks about writing JSX syntax in .ts files so you can get the benefits of TypeScript such as static analysis. This would be nice, but this is not the only way to get the benefits of TypeScript. The other way is to write a translator from .jsx to .ts. (This could be a simple change to the existing .jsx to .js translator.) There should also be syntax extensions to specify the state and props fields and their types in an interface. Once translated to .ts, the TypeScript compiler can then verify the usage of props and state fields.

@fdecampredon
Copy link

The discussion so far mostly talks about writing JSX syntax in .ts files so you can get the benefits of TypeScript such as static analysis. This would be nice, but this is not the only way to get the benefits of TypeScript. The other way is to write a translator from .jsx to .ts. (This could be a simple change to the existing .jsx to .js translator.) There should also be syntax extensions to specify the state and props fields and their types in an interface. Once translated to .ts, the TypeScript compiler can then verify the usage of props and state fields.

I have tried this way, but it is a lot more complex than having a fork with jsx support.
Firstly anyway you will need a parser that understand typescript and jsx anyway. Secondly integration of a build step before typescript makes pretty hard to take advantage of the language service. And finally the type-checking for the compiled jsx is not handled pretty well by typescript.

For all this reasons I think jsx-typescript is a lot more safer and easy to manage than having a build step jsx -> ts.

@jbrantly
Copy link
Contributor

@petilon That is exactly what this suggests. I demoed this concept here along with showing some simplistic type checking on state and props. If you're using webpack, super easy to integrate it using ts-jsx-loader.

All of that said, everything @fdecampredon says is true. I get around the parser understanding both TypeScript and JSX because I just regular expressions instead of a parser which has its own set of problems. I also require explicitly marking the JSX which I can certainly understand many people not being a fan of. My approach definitely lacks any sort of language service integration (intellisense, etc). And lastly, the type-checking for props using createElement is definitely not very good. I show some of this in my talk, but I only show the parts that work and skip over the parts that don't.

@bgrieder
Copy link

Just adding a bit of my personal experience to that discussion.
I tried an alternative route which is to separate the JSX templates from the code, using react-templates and attempted to make it work for typescript. The sad conclusion is that is does not work well for the combination Typescript+IntelliJ/Webstorm (even with the latest 1.4 support).

Also, because one needs to import another file (the compiled template), this sometimes end up with circular dependencies which are impossible to solve with the rigid 'import at the top' "feature" of Typescript. A typical exemple is a recursive display of a Menu object.

OTHA, inline solutions like ts-jsx-loader in IntelliJ/WS work great when you use backticks (templates strings). Syntax coloring and auto-completion is available for HTML inside the template string.

@fdecampredon
Copy link

@1two the problem is that intellij don't use the LanguageService, which make it very hard to adapt to different ts version.

@bgrieder
Copy link

@fdecampredon Not sure I follow you; do you mean variables/context discovery (in addition to syntax auto-completion) ?

Using this syntax in IJ 14.1/WS 10EAP

render() {
    //language="JSX Harmony"
    return React.jsx( `

    <li>
        <a title={this.props.title}>
            <span>{this.props.title}</span>
        </a>
    </li>

    `)
}

I get syntax coloring and auto-completion on the JSX but nothing on this which is not inferred.
It works for me in nearly all cases but for sure, it will never be as good as direct full support of JSX in both the Typescript compiler and the IJ/WS syntax analyzer.

@fdecampredon
Copy link

No typescript comes with a bundled languageService utilities for editor development.
Last time I checked intellij did not use it.
That's why with visual studio/atom-typescript etc you can directly use different typescript fork (like jsx-typescript) out of the box (as long as they respect language service interface), and that's why it does not work with intellij.

@Ciantic
Copy link

Ciantic commented Mar 30, 2015

Couldn't JSX just simply have escaping possibility:

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

Or in case of type-assertions:

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

Either way, I think JSX transformer should have escaping.

@Rajeev-K
Copy link

Rajeev-K commented Jun 3, 2015

Another possibility is to generate TypeScript interfaces from .jsx files. Here's a tool to do this: https://github.com/fuselabs/jsxtyper

@quantuminformation
Copy link

Do you think there will be a solution that will allow TypeScript to be used with reflux?

@sophiebits
Copy link
Collaborator

@quantuminformation Probably a better question for the reflux folks.

@sophiebits
Copy link
Collaborator

TypeScript may start supporting JSX per microsoft/TypeScript#3203. We don't have plans to develop JSX support separately from that, so I'm going to close out this issue – but feel free to continue discussing here or on https://discuss.reactjs.org/ if helpful.

@sophiebits
Copy link
Collaborator

See http://www.jbrantly.com/typescript-and-jsx/ for an update from @jbrantly on the current state of affairs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests