Typescript: Add a simple jsx mode

Created on 10 Jan 2017  路  8Comments  路  Source: microsoft/TypeScript

This is not a design proposal, just a preliminary inquiry to see if there's any interest.

I've spent a couple of nights reading this and that and working hard to arrive at some kind of understanding of the type-system mechanics underlying JSX code generation, and even asked.

It's incomprehensible to me - but I also don't know React, and don't frankly want to. And I don't think this is just me being a lazy idiot - as Basarat points out in "TypeScript Deep Dive", non-React JSX "is for advanced UI framework authors", which to me sounds like pointing out the very regrettable fact that this feature isn't simple enough for most people to use or understand, except perhaps to the point of installing @types/react and working with React. I think we can mostly agree that the language feature itself is not very accessible?

It seems this language feature was designed closely referencing React, and is heavily colored by React's design and implementation approach - and as a language feature, it is far more complex than it needs to be.

For comparison, please take a close look at "Custom XML Literals" feature described in the documentation for Skew.

Please load the code sample on the page, hover over the Skew source-code to inspect inferred types of variables, etc. and have a look at the JS output - as you can see, this is both simple, safe and generic.

I understood this and was using it literally after five minutes, with type-safety and all - contrast that with several nights of tinkering with the JSX feature in TypeScript, reading the same documentation and drilling through the React declarations and other code that uses this feature, and I still feel I am no closer to understanding it.

Apart from being much simpler and easier to understand, it also appears to be considerably more flexible - it's really mainly syntactic sugar for constructing object-graphs and initializing properties, which is much easier to understand, and creates more freedom for exploring alternate patterns, not just those defined by React and JSX.

It's also much more powerful, allowing statements such as if/for/while to be carried out inline, expressions and literal values to seamlessly weave into and between initializations.

I realize this is a compiled language with very different design objectives - and very different constraints, given that this does not attempt to be a superset of JavaScript. But the idea behind it is so much more elegant, so much simpler, so much easier to understand, and likely even more powerful than JSX.

"Intrinsic elements" aren't supported, but that's about the only drawback I can see, and even if this had to be supported somehow, I'm sure the net result would be much simpler than what we inherited from React.

In my opinion, language features should be simple and generic, and this is neither - referencing React, we inherited all the complexity of that, and ended up with something that is (at least somewhat) locked into certain patterns and ideas, such as the idea that everything needs to pass through a createElement factory-function, or that element-attributes are defined a reserved property (such as props) and so on.

Please consider adding an additional jsx mode, or perhaps an alternate file-extension, implementing a simple XML-style object initialization feature that is designed for simplicity, and for the language, rather than for an existing framework.

And please note that --jsx=preserve is not what I'm asking for - simply passing on the problem does not address any of the issues highlighted above in terms of simplicity and type-safety.

There is something to be said for language features you can understand and exploit almost immediately - object initializations in Skew feel completely natural and perfectly "at home" in the language, whereas JSX feels like another world (React) and more like a solution to a very specific problem (React integration) than a language feature engineered for general purposes.

I feel that we're missing out on a potentially very simple and powerful language feature with a lot of applications beyond simply using or emulating React, so I hope you'll at least discuss and take this under consideration internally.

Out of Scope Suggestion

Most helpful comment

I have the impression (formed from admittedly limited insight into the internal workings of Typescript) that simplifying Typescripts handling of JSX syntax could make both the compiler/typechecker simpler as well as support new use cases.

That's essentially where I'm coming from. This feature is much too complex - a simpler, more general language feature would be much more generally useful.

From the offset, I've been pointing out how this feature did not emerge from language design, but rather from trying to shoehorn the concepts of a specific framework into the language.

A language feature based on design, rather than by certain specific patterns implemented by a specific framework, should (if the design is good) be able to support the specific patterns of a given framework - possibly requiring an adapter or additional layer of some sort, but being fundamentally simpler, and having a much broader set of uses than just supporting certain frameworks.

I keep wishing the TS team would take this one back to the drawing board and figure out the essence of how an XML-like syntax feature needs to work.

Then let the community figure out how to apply that feature, to existing frameworks, and to new and interesting ideas. As it is, there's not much room for free thinking.

All 8 comments

I empathize with the complexity of JSX (having implemented support for it!). Understanding JSX (as it's used in React) without understanding React first is a bit of a fool's errand - it's much easier to write or look at a few React Components and see how they correlate to the JSX, than vice versa.

There's also already a fairly robust set of other JSX behaviors you can use depending on how you write the JSX namespace. For example, you don't need to use props - the element type itself can represent the allowed set of attributes. Or you can allow all arbitrary tag names, or you can restrict what kinds of classes can back the XML object. I've seen some prototypes internally that basically make XAML using JSX in TypeScript - it's quite flexible.

I'm also not sure what a world where these JSX literals don't go through a factory function even looks like - it has to be transpiled to something. The thing is, JS already has a syntax for object initialization: { }. So for this other XML-like syntax to justify its existence, it needs to do something different from { }. And at the point that you diverge from the built-in behavior, the set of options opens up to the infinite set, and we have to pick and choose which behaviors are plausible and useful. While the JSX authors claim it to be a React-agnostic grammar, so far the only frameworks using it are using it in a React-like way, because that seems to be one of the only ways to have new behavior that justifies not using { } to initialize your objects.

Understanding JSX (as it's used in React) without understanding React first is a bit of a fool's errand

That's my critique, more or less - rather than designing a language feature anyone can understand and use, this feature explicitly references and models React, which is just one framework, and a complex one at that.

I'm also not sure what a world where these JSX literals don't go through a factory function even looks like - it has to be transpiled to something

Well, take a look at what Skew does.

JS already has a syntax for object initialization: { }. So for this other XML-like syntax to justify its existence, it needs to do something different from { }

What Skew does is not simply object literals, it's calling constructors and setting properties. It does this with type-safety.

If I wanted to pass the whole object graph through some kind of factory-function afterwards, of course I can do that - that part doesn't need to be baked-in, and it doesn't need to depend on one specific module declared declared in tsconfig.json ahead of time.

It's not even a given that any factory function is required at all - as in the example in Skew's documentation, it's perfectly feasible to design objects that don't require any pass through an external factory. In fact I'd say that's mostly how we design objects normally in everyday OO programming - rarely do I construct objects and then pass them all through the same factory-function, that's a pattern employed by React specifically.

While the JSX authors claim it to be a React-agnostic grammar, so far the only frameworks using it are using it in a React-like way,

Again, this confirms my critique - this feature is primarily useful for React and "React-like" frameworks.

because that seems to be one of the only ways to have new behavior that justifies not using { } to initialize your objects.

Well, I mean, I could just alias React.createElement as, say, r then, and the same is true for React and JSX, right?

class ShoppingList extends React.Component {
  let r = React.createElement;
  render() {
    return (
      r('div', {className:"shopping-list"},
        r('h1', `Shopping List for ${this.props.name}`,
          r('ul',
            ...
          )
        )
      )
    );
  }
}

So I'd argue this is more about syntax and less about behavior - if you'd look at Skew, you'll see that it really isn't a feature that provides behavior, so much as mere syntactic convenience for creation of deep object graphs, which is otherwise messy and unreadable.

I think the argument in favor of something XML-like vs object literals is something akin to arguing XML vs JSON - sure, XML is full of completely redundant </div> tags, JSON works just as well... just that all those curly braces make JSON pretty much unreadable for object-graphs with the depth that's typical of HTML documents.

Should a feature like this define (or imply) behavior?

In my opinion, it should not.

I'd prefer a simple syntax for object-graphs that lets me define the behavior.

If for React that means I have to write a generic function that takes that object-graph and passes every object/value through React.createElement(), I don't think that sounds too scary.

Also, think of the all the other things you could do with a feature like this besides just views - for example, I could picture something deeply nested like configuration for a dependency injection container getting bootstrapped this way. Audio processing components being wired together into complex DSP graphs playing via WebAudio. Configuration for a server-side routing or middleware facility. 2D texture filters or 3D transformations on geometric primitives for dynamic creation of complex 3D objects or scenes. Language grammars with parser combinators reflecting the logical structure of a language syntax.

There is so much more we could do with a language feature without behavior baked-in (or implied) that we can't do with something that was designed for views, but of course we could also do views, and likely developers would find new and interesting uses for this feature that we can't even think of.

What we can do with JSX and a React-like feature is limited (more or less) to "something React-like", and to me, that just doesn't sound like something that should be a language feature in the first place.

I mean, I'm sure it's great if all you want to do is use React (or something React-like) but it's not much fun (or use) for those of us with ideas - it's not an inspiring feature with much room for creativity.

I don't think that createElement is so bad. It's signature (on a logical level) is fairly straight forward:

function createElement(
    elementType: Class,      // the `tag`
    properties: Props,       // the attributes of the element
    ...children: Element[]   // child elements
): Element

This is actually the most _technology agnostic_ solutions because you can do whatever you want in the function and return anything as long as it's an Element. It doesn't assume classes, functions, POJOs or anything. All it does is describing the properties of an XML element.

The biggest problem I see is that type-checking JSX is not a simple desugarring to function calls and subsequently type-checking the functions. There is a bespoke baked-in JSX type-checker, instead. It creates a discrepancy between xml-like syntax and manually calling the factory function as not all valid uses of the first are also valid uses of the second (e.g. discriminated unions and aria-). I've not experimented with jsxFactory and custom objects, but I suspect troubles will arise.

I just ran into a similar problem. I'm trying to build a Cycle.JS app using snabbdom-jsx.

From reading the snabbdom docs, the following should be sufficient:

  • configure jsxFactory to be html
  • import {html} from 'snabbdom-jsx
  • write an ambient module declaration (snabbdom is TS, snabbdom-jsx isn't):
    typescript declare module 'snabbdom-jsx' { import {VNode} from 'snabbdom/vnode' export function html(type:string, props?:{}, children?:(VNode|string)[]):VNode }
    The signature here is probably not completely correct, but I can make my point anyway.

Given this setup, the code <h1>foo</h1> should be treated as html(h1, {}, 'foo') of type VNode. Typescript complaining about missing definitions for JSX.Element/JSX.IntrinsicElement makes no sense from a practical perspective.

I tried to work around this by declaring an ambient JSX namespace that aliases Element to VNode, but since the VNode type is exported from a module, it is impossible to reference from an ambient definition (I could not find a way after an hour of googling).

The coupling between JSX as a syntax and JSX-types makes custom JSX-factories quite useless for me :/ I could of course declare the required JSX types as any and be done with it, but I'd really like to have JSX expressions typechecked.

I have the impression (formed from admittedly limited insight into the internal workings of Typescript) that simplifying Typescripts handling of JSX syntax could make both the compiler/typechecker simpler as well as support new use cases.

I have the impression (formed from admittedly limited insight into the internal workings of Typescript) that simplifying Typescripts handling of JSX syntax could make both the compiler/typechecker simpler as well as support new use cases.

That's essentially where I'm coming from. This feature is much too complex - a simpler, more general language feature would be much more generally useful.

From the offset, I've been pointing out how this feature did not emerge from language design, but rather from trying to shoehorn the concepts of a specific framework into the language.

A language feature based on design, rather than by certain specific patterns implemented by a specific framework, should (if the design is good) be able to support the specific patterns of a given framework - possibly requiring an adapter or additional layer of some sort, but being fundamentally simpler, and having a much broader set of uses than just supporting certain frameworks.

I keep wishing the TS team would take this one back to the drawing board and figure out the essence of how an XML-like syntax feature needs to work.

Then let the community figure out how to apply that feature, to existing frameworks, and to new and interesting ideas. As it is, there's not much room for free thinking.

I agree with the gist of this suggestion, but like the OP I'm a bit stuck trying to suggest a solution.

I've been tinkering with my own "react-like" rendering engine which renders object literals instead of function calls. It's a bit of a wonky idea, but of course a few days into it I was trying to add JSX support. What I've come up so far is a fork of TypeScript with a clunky option to output object literals.

It would be awesome if I didn't have to use this fork, although I understand the probability that TS adds a compiler option just for my random side-project is approximately zero.

And while it's completely logical that JSX tools should prioritize React, the project that made it popular in the first place (and one that I like and use a lot), it still kinda sucks that JSX support is coupled to all of its conventions. A configurable visitJsxOpeningLikeElement function in compiler/transformers/jsx.ts would do the trick. But I haven't been able to figure out that approach yet, and is there even a way to do this without it negatively affecting the project as a whole? One thing I really like about TypeScript (compared with Babel) is its relative consistency, while supporting a lot of useful compiler features and options. But this case is one where Babel, with all its plugin support, is much better adapted.

So I'm pretty much stuck here and am fairly convinced this is a classic cathedral/bazaar conundrum.

@janv

From reading the snabbdom docs, the following should be sufficient:

configure jsxFactory to be html

 import {html} from 'snabbdom-jsx'

write an ambient module declaration (snabbdom is TS, snabbdom-jsx isn't):

declare module 'snabbdom-jsx' {
  import {VNode} from 'snabbdom/vnode' 
  export function html(type:string, props?:{}, children?:(VNode|string)[]):VNode
}

The signature here is probably not completely correct, but I can make my point anyway.
Given this setup, the code

foo

should be treated as html(h1, {}, 'foo') of type VNode. Typescript complaining about missing definitions for JSX.Element/JSX.IntrinsicElement makes no sense from a practical perspective.

I tried to work around this by declaring an ambient JSX namespace that aliases Element to VNode, but since the VNode type is exported from a module, it is impossible to reference from an ambient definition (I could not find a way after an hour of googling).

I'm not arguing against the premise here, but you can get the code in your example to typecheck by just breaking it into multiple files

// globals.ts

declare module 'snabbdom-jsx' {
  import vnode from 'snabbdom/vnode';
  export const html: typeof vnode;
}

```ts
// augmentations.ts

import {VNode} from 'snabbdom/vnode';

declare global {
namespace JSX {
export type IntrinsicElements = {
[P in keyof HTMLElementTagNameMap]: Partial;
}
export type Element = Partial;
}
}

then you can write
```ts
/// a.tsx
import {html} from 'snabbdom-jsx';

const a = <h1>foo</h1>;

the only problem is that if there are no attributes, the emit will use null

"use strict";
import { html } from 'snabbdom-jsx';
const a = html("h1", null, "foo");

which is very annoying but easy enough to work around.

This is very much a chicken and egg... I highly doubt JSX support would have been added to TypeScript outside of the fact that it became a defacto standard because of React. Any other solution would need to have a huge amount of defacto support to make it worth while.

I have never been a fan of JSX, but putting that aside, for @dojo 2, where we have functions that return component virtual DOM nodes and hyperscript virtual DOM nodes, using what is provided by TypeScript to support the syntax was really straight forward. So for me, I would already consider the implementation of JSX in TypeScript as a general language feature.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

DanielRosenwasser picture DanielRosenwasser  路  3Comments

bgrieder picture bgrieder  路  3Comments

kyasbal-1994 picture kyasbal-1994  路  3Comments

seanzer picture seanzer  路  3Comments

uber5001 picture uber5001  路  3Comments