JSX in TypeScript #3203

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

Comments

Projects
None yet
@RyanCavanaugh
Member

RyanCavanaugh commented May 18, 2015

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

This comment has been minimized.

Show comment
Hide comment
@RyanCavanaugh

RyanCavanaugh May 18, 2015

Member

@fdecampredon has been helping with this

Member

RyanCavanaugh commented May 18, 2015

@fdecampredon has been helping with this

@jbrantly

This comment has been minimized.

Show comment
Hide comment
@jbrantly

jbrantly May 19, 2015

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?

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

This comment has been minimized.

Show comment
Hide comment
@tinganho

tinganho May 19, 2015

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?

Contributor

tinganho commented May 19, 2015

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

This comment has been minimized.

Show comment
Hide comment
@icetraxx

icetraxx May 19, 2015

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.

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

This comment has been minimized.

Show comment
Hide comment
@icetraxx

icetraxx May 19, 2015

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

@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

This comment has been minimized.

Show comment
Hide comment
@jeffmo

jeffmo 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)}.

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

This comment has been minimized.

Show comment
Hide comment
@RyanCavanaugh

RyanCavanaugh May 19, 2015

Member

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.

Member

RyanCavanaugh commented May 19, 2015

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

This comment has been minimized.

Show comment
Hide comment
@jbrantly

jbrantly May 19, 2015

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?

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

This comment has been minimized.

Show comment
Hide comment
@tinganho

tinganho May 19, 2015

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.

Contributor

tinganho commented May 19, 2015

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

This comment has been minimized.

Show comment
Hide comment
@MgSam

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

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

This comment has been minimized.

Show comment
Hide comment
@fdecampredon

fdecampredon May 19, 2015

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

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

@fdecampredon

This comment has been minimized.

Show comment
Hide comment
@fdecampredon

fdecampredon May 19, 2015

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

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

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

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

This comment has been minimized.

Show comment
Hide comment
@fdecampredon

fdecampredon May 19, 2015

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.

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

This comment has been minimized.

Show comment
Hide comment
@fdecampredon

fdecampredon May 19, 2015

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

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

This comment has been minimized.

Show comment
Hide comment
@RyanCavanaugh

RyanCavanaugh May 19, 2015

Member

Placeholder comment for things I have moved to other threads:

Member

RyanCavanaugh commented May 19, 2015

Placeholder comment for things I have moved to other threads:

@RyanCavanaugh

This comment has been minimized.

Show comment
Hide comment
@RyanCavanaugh

RyanCavanaugh May 19, 2015

Member

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.

Member

RyanCavanaugh commented May 19, 2015

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

This comment has been minimized.

Show comment
Hide comment
@fdecampredon

fdecampredon May 20, 2015

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.

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

This comment has been minimized.

Show comment
Hide comment
@fdecampredon

This comment has been minimized.

Show comment
Hide comment
@fdecampredon

fdecampredon May 22, 2015

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[]  } 
};

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 from Typing for JSX/React to Decoupled typing for JSX and React May 26, 2015

@RyanCavanaugh

This comment has been minimized.

Show comment
Hide comment
@RyanCavanaugh

RyanCavanaugh May 26, 2015

Member

(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" />;
Member

RyanCavanaugh commented May 26, 2015

(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

This comment has been minimized.

Show comment
Hide comment
@thorn0

thorn0 May 26, 2015

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

There is React Native.

thorn0 commented May 26, 2015

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

There is React Native.

@mhegazy

This comment has been minimized.

Show comment
Hide comment
@mhegazy

mhegazy May 26, 2015

Contributor

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

This comment has been minimized.

Show comment
Hide comment
@RyanCavanaugh

RyanCavanaugh May 26, 2015

Member

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

Member

RyanCavanaugh commented May 26, 2015

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

This comment has been minimized.

Show comment
Hide comment
@thorn0

thorn0 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

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

@billti

This comment has been minimized.

Show comment
Hide comment
@billti

billti May 27, 2015

Member

Great stuff! Thanks for pushing this forward.

A few comments:

  • To @mhegazy 's comment, perhaps IntrinsicElements is a more meaningful name.
  • Having a string indexer on here seems of limited value, if that means every custom component would then have an identical set of properties/attributes. (Does this seem a likely use case in real code?)
  • I don't quite get the value of JSX.ElementAttributesProperty. If this is a singleton definition, is the expectation of a common set of attributes across all JSX elements?
  • In a similar vein, the goal here is to decouple JSX from React, yet having all the attributes surfaces on a 'props' property bag seems quite React specific. (But maybe this isn't solvable in an entirely decoupled way).
Member

billti commented May 27, 2015

Great stuff! Thanks for pushing this forward.

A few comments:

  • To @mhegazy 's comment, perhaps IntrinsicElements is a more meaningful name.
  • Having a string indexer on here seems of limited value, if that means every custom component would then have an identical set of properties/attributes. (Does this seem a likely use case in real code?)
  • I don't quite get the value of JSX.ElementAttributesProperty. If this is a singleton definition, is the expectation of a common set of attributes across all JSX elements?
  • In a similar vein, the goal here is to decouple JSX from React, yet having all the attributes surfaces on a 'props' property bag seems quite React specific. (But maybe this isn't solvable in an entirely decoupled way).
@fdecampredon

This comment has been minimized.

Show comment
Hide comment
@fdecampredon

fdecampredon May 27, 2015

This proposal has a major problem for React itself : react elements are not react component instances

class MyComp implements JSX.ElementClass {
    render() {
        return undefined; // NYI
    }
    props: {
        title?: string;
    }
}
var m1 = <MyComp />; 
//transpiled to
var m1 = React.createElement(MyComp);
// equivalent (and might be directly transpiled to in the future)
var m1 = { type: MyComp};

This concept is already confusing enough for people new to React, if typescript consider JSX element type as component instance it might add to the confusion, and miss lead people in false belief.

Also in your proposal all intrinsic element share a same set of props defined in ElementAttributesProperty from what I understand, this a bit limiting, especially when you consider the differences between svg an html element.

This proposal has a major problem for React itself : react elements are not react component instances

class MyComp implements JSX.ElementClass {
    render() {
        return undefined; // NYI
    }
    props: {
        title?: string;
    }
}
var m1 = <MyComp />; 
//transpiled to
var m1 = React.createElement(MyComp);
// equivalent (and might be directly transpiled to in the future)
var m1 = { type: MyComp};

This concept is already confusing enough for people new to React, if typescript consider JSX element type as component instance it might add to the confusion, and miss lead people in false belief.

Also in your proposal all intrinsic element share a same set of props defined in ElementAttributesProperty from what I understand, this a bit limiting, especially when you consider the differences between svg an html element.

@RyanCavanaugh

This comment has been minimized.

Show comment
Hide comment
@RyanCavanaugh

RyanCavanaugh May 27, 2015

Member

@mhegazy / @billti :
Let's 🚲 🏠 the exact naming of the interfaces an intermediate step rather than the first. Maybe it will go away and we won't have to think about it 😉

I think a string indexer is useful for potential non-React things (or as an opt-out for JSX typechecking), especially when that indexer returns any. How would you otherwise model a framework where the set of allowed element names is not closed? You could imagine a framework that decomposed JSX literals into simple objects for serialization -- there would be no gain in constraining the element names in that scenario.

Re: ElementAttributesProperty, I agree this is wonky. Ambient decorators would be a much cleaner solution once we have them.

yet having all the attributes surfaces on a 'props' property bag seems quite React specific

It's not props, it's some property on a type somewhere. If we get some type system syntax for referring to a type in its own declaration, we could wrote that property bag to the object itself. Alternatively, if we switch to defining this behavior via ambient decorators, this could be more expressive.

@fdecampredon good points.

The point about the return type of createElement is well-taken and it's clear this needs to change. Should m1 just be of type { type: {} } ? Or { type: {}, props: {} } ? What does React actually guarantee about the return value of that function?

The react.d.ts file I have in the jsx-dev already distinguishes HTML and SVG element attributes properly. If we decide to go this route, we can clean it up more (e.g. move href to be specific to a or whatever else makes sense).

To address your question in the chat, the things I find overlimiting in checking these elements as if they are function calls are:

  • We'd have no way of erroring on an invalid tag name (because a string-overloaded function must have a fallback name). Maybe fixable by #1003, though that proposal defines a set of close-ended strings that would be really annoying to augment.
  • We'd have no way of erroring on an invalid attribute name (because surplus properties in object literals do not raise an error). #391 doesn't help here because we'd still to allow data-/aria- attributes by some rule.
  • This would hardcode in React's semantics about foo vs Foo being a string / a value name. Opinions seem to differ about how big of a deal this is - I get the sense that the people concerned about us baking in React semantics don't want us to do JSX at all, but I think it's wise not to encode rules from any pre-1.0 framework.
  • Unclear what we would do with namespaced elements (though I haven't addressed this here yet either)
Member

RyanCavanaugh commented May 27, 2015

@mhegazy / @billti :
Let's 🚲 🏠 the exact naming of the interfaces an intermediate step rather than the first. Maybe it will go away and we won't have to think about it 😉

I think a string indexer is useful for potential non-React things (or as an opt-out for JSX typechecking), especially when that indexer returns any. How would you otherwise model a framework where the set of allowed element names is not closed? You could imagine a framework that decomposed JSX literals into simple objects for serialization -- there would be no gain in constraining the element names in that scenario.

Re: ElementAttributesProperty, I agree this is wonky. Ambient decorators would be a much cleaner solution once we have them.

yet having all the attributes surfaces on a 'props' property bag seems quite React specific

It's not props, it's some property on a type somewhere. If we get some type system syntax for referring to a type in its own declaration, we could wrote that property bag to the object itself. Alternatively, if we switch to defining this behavior via ambient decorators, this could be more expressive.

@fdecampredon good points.

The point about the return type of createElement is well-taken and it's clear this needs to change. Should m1 just be of type { type: {} } ? Or { type: {}, props: {} } ? What does React actually guarantee about the return value of that function?

The react.d.ts file I have in the jsx-dev already distinguishes HTML and SVG element attributes properly. If we decide to go this route, we can clean it up more (e.g. move href to be specific to a or whatever else makes sense).

To address your question in the chat, the things I find overlimiting in checking these elements as if they are function calls are:

  • We'd have no way of erroring on an invalid tag name (because a string-overloaded function must have a fallback name). Maybe fixable by #1003, though that proposal defines a set of close-ended strings that would be really annoying to augment.
  • We'd have no way of erroring on an invalid attribute name (because surplus properties in object literals do not raise an error). #391 doesn't help here because we'd still to allow data-/aria- attributes by some rule.
  • This would hardcode in React's semantics about foo vs Foo being a string / a value name. Opinions seem to differ about how big of a deal this is - I get the sense that the people concerned about us baking in React semantics don't want us to do JSX at all, but I think it's wise not to encode rules from any pre-1.0 framework.
  • Unclear what we would do with namespaced elements (though I haven't addressed this here yet either)
@fdecampredon

This comment has been minimized.

Show comment
Hide comment
@fdecampredon

fdecampredon May 27, 2015

React as very strange rules of what is a react element, especially handling key and ref typing might be challenging (however things could be approximated for that)

<div />;
React.createElemen('div', null);
{ type: 'div', props: null};

<div disabled  />;
React.createElemen('div', { disabled: true} );
{ type: 'div', props: { disabled: true }};

<div disabled key="hello" ref={() => void 0}  />;
React.createElemen('div',  { disabled: true, key= "hello",  ref: () => void 0 } );
{ type: 'div', key= "hello",  ref: () => void 0,  props: { disabled: true }};

<div> some children </div>;
React.createElemen('div', null," some children ");
{ type: 'div', props: { children: [" some children "] } }

<MyComponent />;
React.createElemen(MyComponent, null);
{ type: MyComponent, props: null};

Finally here is the type of an React Element (as far as I can express it):

type ReactChildren =  string | ReactElement |  Array<ReactChildren>

type ReactElement = {
  type : string | class,
  props : { children:ReactElementArray,  ...other props },
  key : string | boolean | number,
  ref : string | (el: typeof type) => void
}

For namespaced elements React does not support them at all, esprima-fb support the parsing however.

React as very strange rules of what is a react element, especially handling key and ref typing might be challenging (however things could be approximated for that)

<div />;
React.createElemen('div', null);
{ type: 'div', props: null};

<div disabled  />;
React.createElemen('div', { disabled: true} );
{ type: 'div', props: { disabled: true }};

<div disabled key="hello" ref={() => void 0}  />;
React.createElemen('div',  { disabled: true, key= "hello",  ref: () => void 0 } );
{ type: 'div', key= "hello",  ref: () => void 0,  props: { disabled: true }};

<div> some children </div>;
React.createElemen('div', null," some children ");
{ type: 'div', props: { children: [" some children "] } }

<MyComponent />;
React.createElemen(MyComponent, null);
{ type: MyComponent, props: null};

Finally here is the type of an React Element (as far as I can express it):

type ReactChildren =  string | ReactElement |  Array<ReactChildren>

type ReactElement = {
  type : string | class,
  props : { children:ReactElementArray,  ...other props },
  key : string | boolean | number,
  ref : string | (el: typeof type) => void
}

For namespaced elements React does not support them at all, esprima-fb support the parsing however.

@RyanCavanaugh

This comment has been minimized.

Show comment
Hide comment
@RyanCavanaugh

RyanCavanaugh May 27, 2015

Member

Had a great chat with @fdecampredon on Gitter and @ahejlsberg in the office.

Key takeaways:

  • Kill SpecialAttributePrefixes and replace with a rule that attribute names that aren't legal identifiers (minus the part about keywords not being legal identifiers) are not considered to be errors when they are not found in the element attributes type. This introduces some false negatives (e.g. you write dat-foo when you meant data-foo), but it's not like we can detect data-lengh vs data-length anyway.
  • The type of a JSX expression should be JsxElement<Tag, Attribs, Children> where Tag is the element class type (NB: not element instance type!), Attribs is an object type formed by attributes of the element (as if it were an object literal), and Children is an array type formed by treating the child elements of the element as if they were elements in an array literal. 'JsxText' nodes are considered to be of type string

I'm working on a type system revision now. @fdecampredon is working on the parsing and is not blocked by the as operator PR.

Member

RyanCavanaugh commented May 27, 2015

Had a great chat with @fdecampredon on Gitter and @ahejlsberg in the office.

Key takeaways:

  • Kill SpecialAttributePrefixes and replace with a rule that attribute names that aren't legal identifiers (minus the part about keywords not being legal identifiers) are not considered to be errors when they are not found in the element attributes type. This introduces some false negatives (e.g. you write dat-foo when you meant data-foo), but it's not like we can detect data-lengh vs data-length anyway.
  • The type of a JSX expression should be JsxElement<Tag, Attribs, Children> where Tag is the element class type (NB: not element instance type!), Attribs is an object type formed by attributes of the element (as if it were an object literal), and Children is an array type formed by treating the child elements of the element as if they were elements in an array literal. 'JsxText' nodes are considered to be of type string

I'm working on a type system revision now. @fdecampredon is working on the parsing and is not blocked by the as operator PR.

@RyanCavanaugh

This comment has been minimized.

Show comment
Hide comment
@RyanCavanaugh

RyanCavanaugh May 27, 2015

Member

The type of a JSX expression should be JsxElement<Tag, Attribs, Children>

Talking with @danquirk, this would have some serious problems if react.d.ts were written in the naive way (interface JsxElement<T, A, C> { type: T; props: A; }) . Consider some code:

function f() {
  if(...) {
    return <div className="blah"/>;
  } else {
    return <div onClick= ... />;
  }
}

It would be ridiculous to issue an error here because neither return expression is assignable to the other. Similarly if you conditionally returned either a div or span, or if you conditionally returned either a MyComponent or YourComponent, or if you conditionally returned either <div>{someString}</div> or <div>{someNumber}</div>.

We could still go down the generic route, but it doesn't look like this is likely to produce any useful typechecking errors in most frameworks. XML literals seem to be more like content (i.e. string literals) than distinguishable objects in this regard -- writing some examples on the whiteboard, it's very difficult to write code that is unambiguously an error differentiated by the types of two JSX elements.

Some useful reference points from repos in the wild. It took me a while to find these because, informatively, nearly every JSX element appears in the return expression of a render function!

Member

RyanCavanaugh commented May 27, 2015

The type of a JSX expression should be JsxElement<Tag, Attribs, Children>

Talking with @danquirk, this would have some serious problems if react.d.ts were written in the naive way (interface JsxElement<T, A, C> { type: T; props: A; }) . Consider some code:

function f() {
  if(...) {
    return <div className="blah"/>;
  } else {
    return <div onClick= ... />;
  }
}

It would be ridiculous to issue an error here because neither return expression is assignable to the other. Similarly if you conditionally returned either a div or span, or if you conditionally returned either a MyComponent or YourComponent, or if you conditionally returned either <div>{someString}</div> or <div>{someNumber}</div>.

We could still go down the generic route, but it doesn't look like this is likely to produce any useful typechecking errors in most frameworks. XML literals seem to be more like content (i.e. string literals) than distinguishable objects in this regard -- writing some examples on the whiteboard, it's very difficult to write code that is unambiguously an error differentiated by the types of two JSX elements.

Some useful reference points from repos in the wild. It took me a while to find these because, informatively, nearly every JSX element appears in the return expression of a render function!

@RyanCavanaugh

This comment has been minimized.

Show comment
Hide comment
@RyanCavanaugh

RyanCavanaugh May 29, 2015

Member

Notes from latest meeting:

  • Remove all unneeded levels of indirection from IntrinsicElements (constructors, props, etc) - the declared property type should be the element attributes type
  • The scanner should preserve all whitespace in JSXText; the emitter can handle the trimming logic to match whatever React semantics are
  • We will support JSX Preserve emit (--jsx preserve) and whatever the latest React emit logic is (--jsx react) with no back compat promise. Since React emit logic is well-isolated, we can rev the latest TypeScript release with a new .version if React unexpectedly ships a new supported version with different emit. Users of non-bleeding-edge React will need to use --jsx preserve and transform with the JSX tools.
Member

RyanCavanaugh commented May 29, 2015

Notes from latest meeting:

  • Remove all unneeded levels of indirection from IntrinsicElements (constructors, props, etc) - the declared property type should be the element attributes type
  • The scanner should preserve all whitespace in JSXText; the emitter can handle the trimming logic to match whatever React semantics are
  • We will support JSX Preserve emit (--jsx preserve) and whatever the latest React emit logic is (--jsx react) with no back compat promise. Since React emit logic is well-isolated, we can rev the latest TypeScript release with a new .version if React unexpectedly ships a new supported version with different emit. Users of non-bleeding-edge React will need to use --jsx preserve and transform with the JSX tools.

@NoelAbrahams NoelAbrahams referenced this issue in knockout/knockout May 31, 2015

Closed

JSX + ko components ? #1663

@RyanCavanaugh

This comment has been minimized.

Show comment
Hide comment
@RyanCavanaugh

RyanCavanaugh Jun 1, 2015

Member

All-up spec for conformance testing purposes

This refers to some production names from https://facebook.github.io/jsx/

Given an expression of the form <N />, where N is any identifier or dotted identifier:

  • Attempt intrinsic lookup:
    • Determine A, the element attributes type:
      • If the JSX.IntrinsicElements type does not exist, A is an implicit any
      • If the property named N exists in the type JSX.IntrinsicElements, A is the type of that property
      • If JSX.InstrinsicElements has a string indexer, A is the type of that indexer
    • Otherwise instrinsic lookup fails
  • If intrinsic lookup failed:
    • Look up the value N.
      • Error if no value named N exists in the current scope
    • C, the element class type, is the type of the value N
    • Determine E, the element instance type:
      • If C is any, E is any
      • Otherwise, E is:
        • The union type of the return types of C's construct signatures, if C has at least 1 construct signature
        • The union type of the return types of C's call signatures, if C has at least 1 call signature and no construct signatures
        • An error is issued if C has no call or construct signatures
    • Error if E is not an object type and is not any
    • If the type JSX.ElementClass exists, check that E is assignable to JSX.ElementClass
    • Determine A, the element attributes type:
      • If E is any, A is any
      • If the type JSX.ElementAttributesProperty does not exist, A is any
      • If JSX.ElementAttributesProperty has 0 properties, A is E
      • If JSX.ElementAttributesProperty has 1 property P, A is the type of E.P
      • If JSX.ElementAttributesProperty has more than 1 property, issue an error
  • The type of the expression is JSX.Element
    • It is an error if the type JSX.Element does not exist

<foo x={y} {...s} />

  • Let A be the element attributes type derived from foo
  • Process the attributes in right to left order:
    • If the attribute is a non-spread attribute named P initialized with an expression e:
      • If A has a property P:
        • Process e with the contextual type of the type of A.P.
        • It is an error if e is not assignable to A.P.
      • If A does not have a property P:
        • Process e without a contextual type
        • If the name of P is a valid JavaScript identifier, issue an error
        • If the name of P is an invalid JavaScript identifier because it is a reserved word, issue an error
    • If the attribute is a spread attribute initialized with an expression e:
      • Process e with the contextual type A
      • For each property P in the apparent type of e:
        • If an attribute assignment with the name P has already occurred (either as an explicit attribute or as part of another spread attribute) in the containing element, nothing happens
        • Otherwise, if A has a property P:
          • It is an error if e.P is not assignable to A.P.
  • After all attributes have been processed, issue an error if any required property in A does not have a corresponding attribute or property of a spread attribute
Member

RyanCavanaugh commented Jun 1, 2015

All-up spec for conformance testing purposes

This refers to some production names from https://facebook.github.io/jsx/

Given an expression of the form <N />, where N is any identifier or dotted identifier:

  • Attempt intrinsic lookup:
    • Determine A, the element attributes type:
      • If the JSX.IntrinsicElements type does not exist, A is an implicit any
      • If the property named N exists in the type JSX.IntrinsicElements, A is the type of that property
      • If JSX.InstrinsicElements has a string indexer, A is the type of that indexer
    • Otherwise instrinsic lookup fails
  • If intrinsic lookup failed:
    • Look up the value N.
      • Error if no value named N exists in the current scope
    • C, the element class type, is the type of the value N
    • Determine E, the element instance type:
      • If C is any, E is any
      • Otherwise, E is:
        • The union type of the return types of C's construct signatures, if C has at least 1 construct signature
        • The union type of the return types of C's call signatures, if C has at least 1 call signature and no construct signatures
        • An error is issued if C has no call or construct signatures
    • Error if E is not an object type and is not any
    • If the type JSX.ElementClass exists, check that E is assignable to JSX.ElementClass
    • Determine A, the element attributes type:
      • If E is any, A is any
      • If the type JSX.ElementAttributesProperty does not exist, A is any
      • If JSX.ElementAttributesProperty has 0 properties, A is E
      • If JSX.ElementAttributesProperty has 1 property P, A is the type of E.P
      • If JSX.ElementAttributesProperty has more than 1 property, issue an error
  • The type of the expression is JSX.Element
    • It is an error if the type JSX.Element does not exist

<foo x={y} {...s} />

  • Let A be the element attributes type derived from foo
  • Process the attributes in right to left order:
    • If the attribute is a non-spread attribute named P initialized with an expression e:
      • If A has a property P:
        • Process e with the contextual type of the type of A.P.
        • It is an error if e is not assignable to A.P.
      • If A does not have a property P:
        • Process e without a contextual type
        • If the name of P is a valid JavaScript identifier, issue an error
        • If the name of P is an invalid JavaScript identifier because it is a reserved word, issue an error
    • If the attribute is a spread attribute initialized with an expression e:
      • Process e with the contextual type A
      • For each property P in the apparent type of e:
        • If an attribute assignment with the name P has already occurred (either as an explicit attribute or as part of another spread attribute) in the containing element, nothing happens
        • Otherwise, if A has a property P:
          • It is an error if e.P is not assignable to A.P.
  • After all attributes have been processed, issue an error if any required property in A does not have a corresponding attribute or property of a spread attribute
@RyanCavanaugh

This comment has been minimized.

Show comment
Hide comment
@RyanCavanaugh

RyanCavanaugh Jun 1, 2015

Member

Main open question at this point is what the behavior should be when some of these well-known interfaces are missing.

    1. JSX.IntrinsicElements is missing, we should:
    • a) intrinsic lookup always succeeds with an implicit any
    • b) intrinsic lookup always succeeds with an any
    • c) intrinsic lookup always fails
    • d) issue an error
    1. JSX.ElementClass is missing, we should:
    • a) Pretend we saw interface ElementClass { } instead
    • b) Pretend we saw interface ElementClass { } instead and issue an implict any warning?
    • c) Issue an error
    1. JSX.Element is missing, we should:
    • a) Type the expression as any
    • b) Type the expression as implicit any
    • c) Issue an error
    1. JSX.ElementAttributesType is missing, we should:
    • a) A is implicit any
    • b) A is any
    • c) Let A = E
    • d) Issue an error

The guiding principle is that we want to issue a single kind of error message so that people have something straightforward to search for.

Member

RyanCavanaugh commented Jun 1, 2015

Main open question at this point is what the behavior should be when some of these well-known interfaces are missing.

    1. JSX.IntrinsicElements is missing, we should:
    • a) intrinsic lookup always succeeds with an implicit any
    • b) intrinsic lookup always succeeds with an any
    • c) intrinsic lookup always fails
    • d) issue an error
    1. JSX.ElementClass is missing, we should:
    • a) Pretend we saw interface ElementClass { } instead
    • b) Pretend we saw interface ElementClass { } instead and issue an implict any warning?
    • c) Issue an error
    1. JSX.Element is missing, we should:
    • a) Type the expression as any
    • b) Type the expression as implicit any
    • c) Issue an error
    1. JSX.ElementAttributesType is missing, we should:
    • a) A is implicit any
    • b) A is any
    • c) Let A = E
    • d) Issue an error

The guiding principle is that we want to issue a single kind of error message so that people have something straightforward to search for.

@alanouri

This comment has been minimized.

Show comment
Hide comment
@alanouri

alanouri Jun 30, 2015

@RyanCavanaugh can you summarize how this landed and how we can use it? Or is your original post still accurate and we should just read that?

@RyanCavanaugh can you summarize how this landed and how we can use it? Or is your original post still accurate and we should just read that?

@RyanCavanaugh

This comment has been minimized.

Show comment
Hide comment
@RyanCavanaugh

RyanCavanaugh Jun 30, 2015

Member

I'm working on docs now. The original post is not 100% accurate, but is reasonably close. I'll post here when we have something more authoritative.

Member

RyanCavanaugh commented Jun 30, 2015

I'm working on docs now. The original post is not 100% accurate, but is reasonably close. I'll post here when we have something more authoritative.

@basarat

This comment has been minimized.

Show comment
Hide comment
@basarat

basarat Jun 30, 2015

Contributor

FWIW I just pushed a release of atom-typescript that allows you to use .tsx files

Version v4.8.0 🌹

Thanks for the great work guys ❤️

Contributor

basarat commented Jun 30, 2015

FWIW I just pushed a release of atom-typescript that allows you to use .tsx files

Version v4.8.0 🌹

Thanks for the great work guys ❤️

@RyanCavanaugh

This comment has been minimized.

Show comment
Hide comment
@RyanCavanaugh

RyanCavanaugh Jun 30, 2015

Member

@basarat making us look like a bunch of 🐢s!

Member

RyanCavanaugh commented Jun 30, 2015

@basarat making us look like a bunch of 🐢s!

@basarat

This comment has been minimized.

Show comment
Hide comment
@basarat

basarat Jun 30, 2015

Contributor

making us look like a bunch of 🐢s!

I just think of myself as a cheerleader \o/ . You guys are the 🗿 ⭐️s

Contributor

basarat commented Jun 30, 2015

making us look like a bunch of 🐢s!

I just think of myself as a cheerleader \o/ . You guys are the 🗿 ⭐️s

@jbrantly

This comment has been minimized.

Show comment
Hide comment
@jbrantly

jbrantly Jul 4, 2015

  • JSX.Element is missing, we should:
    • Type the expression as any
    • Type the expression as implicit any
    • Issue an error

It seems this behavior was changed to "Type the expression as implicit any" due to the following code in the PR:

if (jsxElementType === undefined) {
  if(compilerOptions.noImplicitAny) {
    error(errorNode, Diagnostics.JSX_element_implicitly_has_type_any_because_the_global_type_JSX_Element_does_not_exist);
  }
}

@RyanCavanaugh can you confirm? Thanks!

jbrantly commented Jul 4, 2015

  • JSX.Element is missing, we should:
    • Type the expression as any
    • Type the expression as implicit any
    • Issue an error

It seems this behavior was changed to "Type the expression as implicit any" due to the following code in the PR:

if (jsxElementType === undefined) {
  if(compilerOptions.noImplicitAny) {
    error(errorNode, Diagnostics.JSX_element_implicitly_has_type_any_because_the_global_type_JSX_Element_does_not_exist);
  }
}

@RyanCavanaugh can you confirm? Thanks!

@jbrantly

This comment has been minimized.

Show comment
Hide comment
@jbrantly

jbrantly Jul 6, 2015

I just posted a fairly detailed writeup on this at http://www.jbrantly.com/typescript-and-jsx/

Since a lot of the information on how this is actually implemented is somewhat scattered across multiple comments I wanted to try and consolidate it in a hopefully easy-to-parse format. Would certainly welcome feedback on anything I got wrong, and I hope it proves useful to those of us testing this out now.

jbrantly commented Jul 6, 2015

I just posted a fairly detailed writeup on this at http://www.jbrantly.com/typescript-and-jsx/

Since a lot of the information on how this is actually implemented is somewhat scattered across multiple comments I wanted to try and consolidate it in a hopefully easy-to-parse format. Would certainly welcome feedback on anything I got wrong, and I hope it proves useful to those of us testing this out now.

@RyanCavanaugh

This comment has been minimized.

Show comment
Hide comment
@RyanCavanaugh

RyanCavanaugh Jul 6, 2015

Member

Fantastic write-up, @jbrantly! Comprehensive, great examples, and up-to-date (great job tracking down all the changes the design has gone through). Would you be interested in putting this into the TypeScript wiki and/or Handbook?

Member

RyanCavanaugh commented Jul 6, 2015

Fantastic write-up, @jbrantly! Comprehensive, great examples, and up-to-date (great job tracking down all the changes the design has gone through). Would you be interested in putting this into the TypeScript wiki and/or Handbook?

@jbrantly

This comment has been minimized.

Show comment
Hide comment
@jbrantly

jbrantly Jul 6, 2015

@RyanCavanaugh Absolutely. Just create a PR for the wiki?

jbrantly commented Jul 6, 2015

@RyanCavanaugh Absolutely. Just create a PR for the wiki?

@RyanCavanaugh

This comment has been minimized.

Show comment
Hide comment
Member

RyanCavanaugh commented Jul 6, 2015

Yep

@RyanCavanaugh

This comment has been minimized.

Show comment
Hide comment
@tinganho

This comment has been minimized.

Show comment
Hide comment
@tinganho

tinganho Jul 7, 2015

Contributor

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

Contributor

tinganho commented Jul 7, 2015

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

@RyanCavanaugh

This comment has been minimized.

Show comment
Hide comment
@RyanCavanaugh

RyanCavanaugh Jul 7, 2015

Member

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

Member

RyanCavanaugh commented Jul 7, 2015

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

This comment has been minimized.

Show comment
Hide comment
@tinganho

tinganho Jul 7, 2015

Contributor

Ok I will try it out 👍

Contributor

tinganho commented Jul 7, 2015

Ok I will try it out 👍

@tinganho

This comment has been minimized.

Show comment
Hide comment
@tinganho

tinganho Jul 7, 2015

Contributor

It worked perfectly!

Contributor

tinganho commented Jul 7, 2015

It worked perfectly!

@mnpenner mnpenner referenced this issue in andreypopp/typescript-loader Jul 7, 2015

Open

TSX support with TypeScript 1.6 #22

@pepaar pepaar referenced this issue in palantir/tslint Jul 9, 2015

Closed

Support for .tsx files #490

@mnpenner

This comment has been minimized.

Show comment
Hide comment
@mnpenner

mnpenner Jul 10, 2015

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.

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 from Decoupled typing for JSX and React to JSX in TypeScript Jul 24, 2015

@gulbanana

This comment has been minimized.

Show comment
Hide comment
@gulbanana

gulbanana Jul 28, 2015

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.

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

This comment has been minimized.

Show comment
Hide comment
@joewood

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

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

This comment has been minimized.

Show comment
Hide comment
@danquirk

danquirk Jul 28, 2015

Member

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

Member

danquirk commented Jul 28, 2015

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

@joewood

This comment has been minimized.

Show comment
Hide comment
@joewood

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

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

This comment has been minimized.

Show comment
Hide comment
@graphnode

graphnode Jul 29, 2015

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

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

@RyanCavanaugh

This comment has been minimized.

Show comment
Hide comment
@RyanCavanaugh

RyanCavanaugh Jul 30, 2015

Member

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

Member

RyanCavanaugh commented Jul 30, 2015

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

This comment has been minimized.

Show comment
Hide comment
@graphnode

graphnode Jul 30, 2015

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

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/DefinitelyTyped that referenced this issue Aug 3, 2015

React: Include required render() function in base Component class.
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.
@bartq

This comment has been minimized.

Show comment
Hide comment
@bartq

bartq Sep 7, 2015

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

bartq commented Sep 7, 2015

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

@joewood

This comment has been minimized.

Show comment
Hide comment
@joewood

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

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.

@bartq

This comment has been minimized.

Show comment
Hide comment
@bartq

bartq Sep 7, 2015

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

bartq commented Sep 7, 2015

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

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.