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

JSX in TypeScript #3203

Closed
RyanCavanaugh opened this issue May 18, 2015 · 80 comments
Closed

JSX in TypeScript #3203

RyanCavanaugh opened this issue May 18, 2015 · 80 comments
Assignees
Labels
Committed The team has roadmapped this issue Fixed A PR has been merged for this issue Suggestion An idea for TypeScript

Comments

@RyanCavanaugh
Copy link
Member

Updated 7/24 with some spec changes

TODO: Add language about string indexers on props types turning off surplus attribute errors

Up-front Requirements

To use JSX expressions in a TypeScript file, you must use the tsx extension. Additionally, during compilation, you must specify a --jsx <value>. Supported values are preserve, which emits the JSX as-is (modulo TypeScript desugaring inside JSX Expressions), or react, which emits React-compatible code according to their most recent stable release.

The JSX namespace

Similar to how we have special types like Array and Number in the global namespace to represent the types of [] and 42, we will add a new global optional namespace JSX with some special types in it.

This namespace has four special types in it. Let's examine them.

JSX.Element

The type of any <expr /> is JSX.Element:

// x is of type JSX.Element
var x = <Something hello="world" />;

If this type does not exist, it is instead an implicit any.

JSX.IntrinsicElements

When checking an expression of the form <foo ...> where foo begins with a lower-case letter, the type JSX.IntrinsicElements is examined. If that type has a property called foo, or a string indexer, then the element attributes type (see below) is the type of that property (or the type of that string indexer). This process is called intrinsic lookup.

JSX.ElementClass

For expressions of the form <Foo ... /> or <namespace.foo .../> we use to value-based lookup: Given an expression <Foo ... >, find a value named Foo in the current lexical scope.

If no value with this name exists, an error is issued. Otherwise, we treat the type of foo as an element class. The element class is the constructor or factory function that produces an instance of an element instance type.

It is an error if the element class is not assignable to the type JSX.ElementClass, or if value-based lookup occurs and no type named JSX.ElementClass exists.

The element instance type is the return types of the first construct signatures (if present), or the first call signature (if no construct signatures are present). If the element type is any, the element instance type is any.

It is an error if the element class has no call or construct signatures.

JSX.ElementAttributesProperty

Given an element instance type, we need to produce a type that lists the allowed attributes for that element. We call this the element attributes type. For example, in a React element <div>, the element attributes type includes properties like id, className, and onClick.

The interface JSX.ElementAttributesProperty defines this process. It may have 0 properties, in which case all attributes are assumed to be valid and of type any, or 1 property, in which case the attributes of the element must map to the properties of the type of that property.

Note that intrinsic lookup is not affected by ElementClass or ElementAttributesProperty.

Attribute Checking

<Something x={expr1} { ...spr } y={expr2} />

Given an element attributes type E derived from Something in the above example, the attributes of the element are checked as follows:

  • If the attribute is a normal attribute P initialized with expr:
    • If E has a property P, process expr with the contextual type of the type of E.P. Otherwise, issue an error.
    • It is an error if expr is not assignable to E.P.
    • Add P to the list of properties seen
  • If the attribute is a spread attribute on expr:
    • For each property P in the apparent type of spr:
      • If a later attribute assignment with the name P occurs (either as an explicit attribute, as shown above with y, or via another spread attribute), nothing happens
      • If E has a property P:
        • It is an error if spr.P is not assignable to E.P.
      • Add P to the list of properties seen
      • Otherwise, nothing happens
  • After all attributes have been processed, issue an error if any required property in E does not have a corresponding entry in the list of properties seen

Non-identifier attributes

If an attribute name would not be a valid identifier in JavaScript code, there need not be a corresponding property in the element attributes type. This exception does not apply to names that are invalid identifiers because they are reserved words (like var).

@RyanCavanaugh RyanCavanaugh added Suggestion An idea for TypeScript In Discussion Not yet reached consensus labels May 18, 2015
@RyanCavanaugh
Copy link
Member Author

@fdecampredon has been helping with this

@jbrantly
Copy link

This does not seem to address how/if <div /> is transformed into Javascript (as you know this has been discussed quite a bit in #296). What is the current thinking there? Will TS have some kind of built-in JSX transformer or will TS leave the JSX intact as-is to be transformed later?

@tinganho
Copy link
Contributor

This is not one of the open questions. But have you been looking at #3022? Would be good to know your thoughts.

It would also be good to know the points on why you are supporting JSX in the first place? As I recall it TS where negative to having a first-class JSX support before and then you change your mind right? Having not stated the main points of why TS decided to support JSX would lead to everyone guessing you are supporting it for greed of more users?

@icetraxx
Copy link

I really hope we can get a transformer for tsx built into the compiler. It would be cumbersome to compile to JavaScript with TSC and then transform it, yet again, to use any tsx files.

@icetraxx
Copy link

@RyanCavanaugh, why not offer the as operator everywhere? It seems more elegant than the current type assertions (which will become problematic anyway if HTML/XML extensions are added to ECMAScript).

@jeffmo
Copy link

jeffmo commented May 19, 2015

The as operator

Over in Flow land, we've found great success with an expression-annotation/downcasting syntax: var a: number = (someVar: number). It's lightweight and works nicely in some other places when combined with inference: var a = { someNumberOrString: ("initialValue": number|string)}.

@RyanCavanaugh
Copy link
Member Author

As an aside, let's please not have any more meta-discussion of the priority or motivations behind JSX support at this time (feel free to log a discussion issue, we'd be happy to talk about it!), the syntax of the as operator (discussion in issue #296 or PR #3201), or anything else you can plausibly find somewhere else where it's being discussed 😄. I assure you this thread will be long enough without those things, and I simply want to move those conversations rather than shut them down or have them get lost in the noise.

Comment round-up follows


This does not seem to address how/if <div /> is transformed into Javascript

&&

I really hope we can get a transformer for tsx built into the compiler

We really don't want to re-implement JSX expansion in the compiler. It's pointless and dangerous to have two implementations of the same thing that must agree on all semantics. People can already use JSXTransformer.js to do this transform at runtime (for simple solutions that don't need top performance), or for projects that do need to compile their JSX ahead-of-time, they should already have a build pipeline for the sake of minification / bundling.

It should be fairly simple for someone to write a tsxc tool that wraps tsc + jsx in one for people who really want a single-invocation, no-runtime experience.


As I recall it TS where negative to having a first-class JSX support before and then you change your mind right?

JSX was previously not one of the highest-priority things for us to do. Now it is (assuming we can get this design fleshed out). Regarding #3022, we covered this in the backlog slog today and I'll be posting comments there when I have time.


why not offer the as operator everywhere?

We will. This is explained in the write-up ("is available in both .ts and .tsx")


Over in Flow land [...]

(v: T) was brought up in the other thread and discussed extensively as a possibility. The postfix position is very attractive and less awkward in many cases, and it's nice that it mirrors other places where type annotations exist, but we didn't like how it looks very similar to the start of an anonymous function and/or a property initializer.

@jbrantly
Copy link

We really don't want to re-implement JSX expansion in the compiler.

This makes sense, but just so it's explicit on what the behavior will be, I'm assuming you're saying that TypeScript output will contain the JSX.

Taking your component example, if TypeScript is given this:

interface MyFormComponent {
  props: {
    myName: string;
    myValue?: number;
  };
}
var MyForm: MyFormComponent = React.createClass(...);
var x = <MyForm myName={42} />;

then it will output this:

var MyForm = React.createClass(...);
var x = <MyForm myName={42} />;

Which could then be fed to the JSX transformer (since it's plain JavaScript at that point).

And going a step further, this:

<div onClick={() => this.doSomething()} />

will output something like this:

var _this = this;
<div onClick={(function() { return _this.doSomething() })} />

The last example just to say that TypeScript-specific syntax contained within JSX will be appropriately transformed into plain JavaScript?

@tinganho
Copy link
Contributor

var x = { a: 3, b: 'foo' };
var y = <MyClass { ...x } />;

How about supporting this too then?

var y = <MyClass { a: 3, b: 'foo'} />;

I find it more ergonomic then typing multiple attribute={value}. It is also more conformant with how a placeholder for the spread argument looks like today.

@MgSam
Copy link

MgSam commented May 19, 2015

@RyanCavanaugh You said you don't want to discuss motivations or prioritization of React support in this thread- but is there somewhere for the community to discuss that? AFAIK, you guys don't post the design meeting notes anymore, so all these sorts of decisions about prioritization are being made behind closed doors with little community discussion.

@fdecampredon
Copy link

Does not using document.createElement will become a problem with SVG elements ?

@fdecampredon
Copy link

Also for spread attributes, how will you handle cases like :

var x = {a: 1, b: 2};
<MyComp a={3} {..x} />;

@fdecampredon
Copy link

Children: Consider if we need to typecheck node contents at all (these are generally very free-form?).

Children type with React is something like:

 interface ReactElementArray extends Array<string | ReactElement | ReactElementArray> {}

However I don't think any will be a problem since this type is quite complicated to typecheck with a structural type system.

@fdecampredon
Copy link

Also could you consider allowing the call for MyForm ?
Some variation around React and some other framework (like mercury) use factory instead of constructor.
So for example we could do something like :

interface MyFormComponent {
  props: {
    myName: string;
    myValue?: number;
  };
}
function MyForm (): MyFormComponent  {....}
var x = <MyForm myName={42} />;

And if we don't find a constructor signature on MyForm we use the call signature

@RyanCavanaugh
Copy link
Member Author

Placeholder comment for things I have moved to other threads:

@RyanCavanaugh
Copy link
Member Author

How about supporting this too then? <MyClass { a: 3, b: 'foo'} />;

If JSX wants to add this syntax, we'd happily support it too, but we're not going to fork their DSL.

... but is there somewhere for the community to discuss that?

Feel free to log an issue asking that question and we can talk about specifics there. Prioritization has a ton of inputs -- technical reasons, business goals, community feedback, enabling partner teams, and so on; it's kind of intractable for us to post general stuff on how these decisions are being made. There is not any meeting with the concrete agenda "Decide which TypeScript features to do next".

Does not using document.createElement will become a problem with SVG elements ?

Great point; I had not considered that case. Any ideas on what we might do instead?

var x = {a: 1, b: 2}; <MyComp a={3} {..x} />;

Last in wins. I had originally thought this should be an error, but you could reasonably have something like this:

interface DefaultThings {
  foo?: number;
  bar?: string;
}
var d: DefaultThings = { };
if(f) d.foo = f;
if(b) d.bar = b;
// Intentional overriding of previously-specified attribute
var x = <MyThing foo={defaultFoo} {...d} />
// Intentional overriding of previously-specified spread attribute
var y = <MyThing {...d} foo={definitelyOverrideFoo} />

Also could you consider allowing the call for MyForm ?

Seems reasonable -- we could consider the construct signatures first, then fall back to the call signatures.

@fdecampredon
Copy link

For document.createElement more I think about it more I think it will be a problem. For example innerHTML is property of HTMLElement, but is not a valid attribute.
At the other end ref key are valid React attributes on any elements, but not valid properties of HTMLElement
Perhaps a solution could be to have a special lib file that define multiple interface for each possible intrinsic element, when using .tsx file without the --nolib option this file would be included, otherwise people would have to write their own.

@RyanCavanaugh
Copy link
Member Author

@fdecampredon
Copy link

What is the type of y in your example ?
Even if we want to use JSX for something else than React I think it should be typed as close as possible as what is transpiled :

//for intrinsic
var y  = <div ref="() => void 0" key="id" id="something"><span /> text</div>;

//type is 
{ 
  type : string; 
  ref: () => void;  
  key: string;  
  props: {  id: string; children: any[]  } 
};

//for element
interface MyFormComponent {
  props: {
    myName: string;
    myValue?: number;
  };
}
var MyForm: MyFormComponent = React.createClass(...);

var y  = (
  <MyComponent ref="() => void 0" key="id" myName="something">
    <span /> text
  </MyComponent>
);

//type is
{ 
  type : MyFormComponent; 
  ref: () => void;  
  key: string;  
  props: {  myName: string; children: any[]  } 
};

@RyanCavanaugh RyanCavanaugh changed the title Typing for JSX/React Decoupled typing for JSX and React May 26, 2015
@RyanCavanaugh
Copy link
Member Author

(moved this section to OP)


Examples of Varied JSX Behavior

All the JSX interfaces start out empty. Let's build on it and see the kinds of behavior we can model.

No JSX Augmentations

declare module JSX { }

// Error, cannot find name 'myElem'
var a1 = <myElem />;

Anything-goes JSX

By adding a string indexer to JSX.Intrinsics, we can allow all JSX expressions to typecheck without error:

declare module JSX {
    export interface Intrinsics {
        [anything: string]: any;
    }
}

// OK
var a1 = <myElem xzx="neat" />;
// Still an error here -- cannot multiply two strings
var a2 = <myElem foo={ 'x' * 'y'} />;

Arbitrary Elements, Constrained Attributes

Maybe we have a DSL with arbitrary element names, but constrained attributes:

declare module JSX {
    export interface ElementAttributesProperty {
        settings;
    }

    export interface RaytracerObject {
        x?: number;
        y?: number;
        z?: number;
        material?: string;
    }

    export interface Intrinsics {
        [anything: string]: {
            new (): { settings: RaytracerObject };
        }
    }
}

// Error, type 'q' does not exist on 'RaytracerObject'
var a1 = <Sphere q="42" />;
// Error, type 'string' is not assignable to 'number'
var a2 = <Plane x="foo" />;
// OK
var a3 = <Cone x={Math.sin(Math.PI / 2) * 3} />

Constrained Elements, Arbitrary Attributes

declare module JSX {
    export interface Intrinsics {
        Dog: any;
        Cat: any;
    }
}

// OK
var a1 = <Dog woof="woof" />
// Error, can't find name 'Pig'
var a2 = <Pig name="Babe" />

Class-based Object Models

declare module JSX {
    interface ElementClass {
        toXML(): string;
    }
    interface ElementAttributesProperty {
        attributes: any;
    }
}

class Room {
    toXML() {
        return '<Room name=' + this.attributes.name + '/>';
    }
    attributes: {
        name?: string;
    }
}

class OtherThing {
    // Forgot to add toXML()!
    attributes: {
        size: number;
    }
}

// OK
var x = <Room name="kitchen" />;
console.log(x.toXML());
// Error, 'toXML' is missing on 'OtherThing'
var y = <OtherThing size="100" />

A Miniaturized React

/// <reference path="src/lib/jsx.d.ts" />

declare module JSX {
    interface SpecialAttributePrefixes {
        'data-': string|number|boolean;
        'aria-': string|number|boolean;
    }

    interface ElementClass {
        render(): any;
    }

    interface ElementAttributesProperty {
        props: any;
    }

    interface Intrinsics {
        div: React.HtmlElementConstructor;
        span: React.HtmlElementConstructor;
    }
}

declare module React {
    interface HtmlElementConstructor {
        new (): HtmlElementInstance;
    }

    interface HtmlElementInstance extends JSX.ElementClass {
        props: ReactHtmlElementAttributes;
    }

    interface ReactHtmlElementAttributes {
        accessKey?: string;
        checked?: string;
        classID?: string;
        className?: string;
        id?: string;
        name?: string;
        ref?: string;
        key?: string;
    }
}

// Using intrinsics
// OK
var x = <div className="myDiv" data-myId="custom" />
// Error, cannot find name 'dvi'
var y = <dvi />
// Error, no property 'classname'
var z  = <span classname="oops" />

// Classes
class MyComp implements JSX.ElementClass {
    render() {
        return undefined; // NYI
    }
    props: {
        title?: string;
    }
}
// OK
var m1 = <MyComp />;
// Error
var m2 = <MyComp tite="missed an L" />;
// Error, cannot find name 'Mycomp'
var m3 = <Mycomp title="hello" />;

@thorn0
Copy link

thorn0 commented May 26, 2015

with the understanding that there are no other major JSX-based frameworks

There is React Native.

@mhegazy
Copy link
Contributor

mhegazy commented May 26, 2015

a few questions:

  • Intrinsics why not call it Elements, JSXElements or ElementTypes? Intrinsics just does not convay much information
  • ElementAttributesProperty, and to some degre SpecialAttributePrefixes, are just holders to metadata, i do not think an interface is the right place for that. i would argue that these can be specified as command line options with appropriate defaults, triple-slash-reference-style comments with appropriate defaults, or even @@ ambient decorators (decorating the property declaration in the ElementClass interface).

@RyanCavanaugh
Copy link
Member Author

React-native has the same type semantics as React, no? We would just need a .d.ts file with a different set of names (View and Image instead of div).

@thorn0
Copy link

thorn0 commented May 26, 2015

Yes, looks like it uses the same JSX transform. But View, Image, TabBarIOS.Item, etc. aren't like div. They're values, see: http://stackoverflow.com/questions/29286899/how-do-i-make-components-in-react-native-without-using-jsx

@RyanCavanaugh
Copy link
Member Author

Yep

@RyanCavanaugh
Copy link
Member Author

@tinganho
Copy link
Contributor

tinganho commented Jul 7, 2015

@RyanCavanaugh is there a way of trying out JSX in VSCode?

@RyanCavanaugh
Copy link
Member Author

Yep, it's very easy:

Replace tsserver.js in C:\Users\<yourname>\AppData\Local\Code\app-0.5.0\resources\app\plugins\vs.language.typescript\lib with a version from built\local in an up-to-date TypeScript enlistment

Edit ticino.plugin.json in C:\Users\<yourname>\AppData\Local\Code\app-0.5.0\resources\app\plugins\vs.language.typescript\; change line 12 to:

"extensions": [".ts", ".js"],

to

"extensions": [".ts", ".tsx", ".js"],

Done!
image

@tinganho
Copy link
Contributor

tinganho commented Jul 7, 2015

Ok I will try it out 👍

@tinganho
Copy link
Contributor

tinganho commented Jul 7, 2015

It worked perfectly!

@mnpenner
Copy link

For anyone else a bit confused as I was, regarding @RyanCavanaugh 's comment: the folder name appears to be app-0.1.2 not app-0.5.0 despite VS Code being currently at version 0.5.0 (dunno why that is!). If yours is different, you can probably just find the one with the highest version number in C:\Users\<username>\AppData\Local\Code.

Also, in order for it to not highlight your JSX as an error ("--jsx flag required"), you need to add a tsconfig.json file to your project. Mine looks like this:

{
    "compilerOptions": {
        "target": "ES6",
        "module": "commonjs",
        "sourceMap": true,
        "jsx": "preserve"
    }
}

The other option for "jsx" being "react", as described in @jbrantly 's excellent blog post.

Then restart Code. It doesn't appear to pick up changes to either the config files, nor tsconfig.

You can use the tsserver.js file from ntypescript if you want. Seems to work fine. I used Agent Ransack to find it because it's buried somewhere deep on my system. I suppose you could also just nab a copy off GitHub.

@RyanCavanaugh RyanCavanaugh changed the title Decoupled typing for JSX and React JSX in TypeScript Jul 24, 2015
@gulbanana
Copy link

this is some excellent work. by handling the complexity of reconciling TS and JSX, you've saved downstream developers like myself a great deal of effort.

@joewood
Copy link

joewood commented Jul 28, 2015

Second that, I converted a project over to use TSX and it works like a charm.
The only downside so far is the lack of type assertions, and having to use as instead. When using this syntax there's no way for the IDE to know the fields in the expression and you lose code completion hints. Not a huge deal, just an annoyance.

@danquirk
Copy link
Member

@joewood what editor are you seeing no completions with as? That just sounds like a bug we should fix.

@joewood
Copy link

joewood commented Jul 28, 2015

@danquirk this is VS Code. To be clear, the completion isn't working inside an object literal. I wouldn't expect it to. So the following would show bar in the object literal if I hit ctrl+space after foo in a .ts file:

let x = <{foo:string, bar?:string}>{ foo:"hello" }

But in a .tsx file, the type isn't asserted until after the literal, so the IDE doesn't know the type:

let x = { foo:"hello" } as {foo:string, bar?:string};

It's not a huge deal. It was just useful for large interfaces, like React's CSSProperties etc...

@graphnode
Copy link

Any chance to add an option where it emits a string?

@RyanCavanaugh
Copy link
Member Author

Any chance to add an option where it emits a string?

Can you clarify with some examples?

Note that JSX can have embedded code in it, so it's not clear that having a string representation of that would be useful

@graphnode
Copy link

I was thinking in the use of jsx highlighting and autocomplete to have simple html in js without any react features.

ngbrown added a commit to ngbrown-forks/DefinitelyTyped that referenced this issue Aug 3, 2015
The new TSX mode in the TypeScript compiler has the requirements that
the element class constructor produces an element instance type that is
assignable to the type JSX.ElementClass.  To meet this Component<P, S>
must declare `render(): JSX.Element`.  Derived classes can be more
specific.

This should resolve microsoft/TypeScript#3203.
@bartosz-k
Copy link

could anyone write step-by-step instructions how to get this working #3203 (comment) from the ground?
Would be great :)

@joewood
Copy link

joewood commented Sep 7, 2015

@Bartq a new version of VSCode is due soon that includes a configuration setting for the TypeScript compiler. This will make the process much easier.

@bartosz-k
Copy link

@joewood - thanks, cool! When, tomorrow? ;)

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Committed The team has roadmapped this issue Fixed A PR has been merged for this issue Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests