Typescript and JSX

JSX support has officially landed in TypeScript! A big thanks to Ryan Cavanaugh and François de Campredon for making this happen. In this post I want to explore how to use JSX and how to make use of TypeScript's #1 feature: static type checking.


When we first started experimenting with React at AssureSign we had already started down the path of using TypeScript. Immediately we ran into a stone wall with JSX. There was a fairly long GitHub issue about it but no real solutions other than "don't use JSX". That was not an option for me so I hacked together a solution. While that mostly worked, it was ugly and not ideal for type checking. François de Campredon eventually created jsx-typescript which showed that TypeScript could in fact support JSX. Then, suddenly, there was a hint that official support might come. Three months later and it has landed.


At this time JSX is not available in a stable release, so you need to get the nightly build. JSX support is available in TypeScript 1.6 and up.

$ npm install typescript

Basic usage

In order to use JSX you must do two things.

  1. Name your files with the .tsx extension
  2. Enable the jsx option

TypeScript ships with two JSX modes: preserve and react. These modes only affect the emit stage. preserve will keep the JSX as part of the output to be further consumed by babel. Additionally the output will have a .jsx file extension. react will emit React.createElement, does not need to go through a JSX transformation before use, and the output has a .js file extension.

Mode Input Output File Extension
preserve <div /> <div /> .jsx
react <div /> React.createElement("div") .js

You can specify this mode either with either the --jsx command line flag or in your tsconfig.json file.

Note: The identifier React is hardcoded, so you must make React available with an uppercase R. react is right out.

as operator

Since TypeScript uses angle brackets for type assertions, there is a conflict when parsing between type assertions and JSX. Consider the following code:

var foo = <foo>bar;  

Is this code creating a JSX element with the content of bar; or is it asserting that bar is of type foo and there is an invalid expression on line 2? To simplify cases like this, angle bracket type assertion is not available in .tsx files. So if the previous code were in a .tsx file it would be interpreted as a JSX element and if in a .ts file it would result in an error. To make up for this loss of functionality a new type assertion operator has been added: as.

var foo = bar as foo;  

The as operator is available in both .ts and .tsx files.

Type Checking

What is JSX in TypeScript without type checking? Thankfully type checking has been well thought out and works beautifully.

First up: Intrinsic elements vs value-based elements. Given a JSX expression <expr />, is expr referring to something intrinsic to the environment (ie, a div or span in a DOM environment) or to a custom component that you've made? This is important for two reasons:

  1. For React, intrinsic elements are emitted as strings, like React.createElement('div'), whereas a component you've created is not: React.createElement(MyComponent).
  2. The types of the attributes being passed in the JSX tag should be looked up differently. Intrinsic element attributes should be known intrinsically whereas components will likely want to specify their own set of attributes (via props).

TypeScript uses the same convention that React does for distinguishing between these: An intrinsic element always begins with a lowercase letter, and a value-based element always begins with an uppercase letter.

Intrinsic elements

Intrinsic elements are looked up on the special interface JSX.IntrinsicElements. By default, if this interface is not specified, then anything goes and intrinsic elements will not be type checked. However, if you specify the interface then intrinsic elements are looked up as a property on the interface.

declare module JSX {  
    interface IntrinsicElements {
        foo: any

<foo />; // ok  
<bar />; // error  

In the above example, foo will work fine but bar will result in an error since it has not been specified on the intrinsic elements interface.

Note: You can also specify a catch-all string indexer on JSX.IntrinsicElements

Value-based elements

Value based elements are simply looked up by identifiers that are in scope.

import MyComponent = require('./myComponent');

<MyComponent />; // ok  
<SomeOtherComponent />; // error  

It is possible to limit the type of a value-based element. However, for this we must introduce two new terms: the element class type and the element instance type.

The element class type is easy. Given <Expr />, the class type is simply the type of Expr. So in the example above, if MyComponent was an ES6 class the class type would be that class. If MyComponent was a factory function, the class type would be that function.

Once the class type is established, the instance type is determined by the union of the return types of the class type's call signatures and construct signatures. So again, in the case of an ES6 class, the instance type would be the type of an instance of that class, and in the case of a factory function, it would be the type of the value returned from the function. Clear as mud? An example might help:

class MyComponent {  
  render() {}

// use a construct signature
var myComponent = new MyComponent();

// element class type => MyComponent
// element instance type => { render: () => void }

function MyFactoryFunction() {  
  return { 
    render: () => {

// use a call signature
var myComponent = MyFactoryFunction();

// element class type => FactoryFunction
// element instance type => { render: () => void }

Now the element instance type is interesting because it must be assignable to JSX.ElementClass or it will result in an error. By default JSX.ElementClass is {}, but it can be augmented to limit the use of JSX to only those types that conform to the proper interface.

declare module JSX {  
  interface ElementClass {
    render: any;

class MyComponent {  
  render() {}
function MyFactoryFunction() {  
  return { render: () => {} }

<MyComponent />; // ok  
<MyFactoryFunction />; // ok

class NotAValidComponent {}  
function NotAValidFactoryFunction() {  
    return {};

<NotAValidComponent />; // error  
<NotAValidFactoryFunction />; // error  

Attribute type checking

The first step to typechecking attributes is to determine the element attributes type. This is slightly different between intrinsic and value-based elements.

For intrinsic elements, it is the type of the property on JSX.IntrinsicElements

declare module JSX {  
  interface IntrinsicElements {
    foo: { bar?: boolean }

// element attributes type for `foo` is `{bar?: boolean}`
<foo bar />;  

For value-based elements, it is a bit more complex. It is determined by the type of a property on the element instance type that was previously determined. Which property, you ask? Why, you get to choose! Simply define JSX.ElementAttributesProperty with a single property. The name of that property is then used.

declare module JSX {  
  interface ElementAttributesProperty {
    props; // specify the property name to use

class MyComponent {  
  // specify the property on the element instance type
  props: {
    foo?: string;

// element attributes type for `MyComponent` is `{foo?: string}`
<MyComponent foo="bar" />  

It should be obvious from the examples above that the element attribute type is used to typecheck the attributes in the JSX. Optional and required properties are supported.

declare module JSX {  
  interface IntrinsicElements {
    foo: { requiredProp: string; optionalProp?: number }

<foo requiredProp="bar" />; // ok  
<foo requiredProp="bar" optionalProp={0} />; // ok  
<foo />; // error, requiredProp is missing  
<foo requiredProp={0} />; // error, requiredProp should be a string  
<foo requiredProp="bar" unknownProp />; // error, unknownProp does not exist  
<foo requiredProp="bar" some-unknown-prop />; // ok, because `some-unknown-prop` is not a valid identifier  

Note: If an attribute name is not a valid JS identifier (like a data-* attribute), it is not considered to be an error if it is not found in the element attributes type.

The spread operator also works:

var props = { requiredProp: 'bar' };  
<foo {...props} />; // ok

var badProps = {};  
<foo {...badProps} />; // error  

The JSX type

Great, so now we can write JSX and have the element and attributes type checked, but what about the type of the JSX itself? By default it is typed as any. You can customize the type by specifying the JSX.Element interface. However, it is not possible to retrieve type information about the element, attributes or children of the JSX from this interface. It is a black box.

Escaping to TypeScript

JSX in JavaScript allows you to escape to JavaScript by using curly braces { }. JSX in TypeScript allows you to do the same thing, but you escape to TypeScript. That means transpilation features and type checking still work when embedded within JSX.

var a = <div>  
  {['foo', 'bar'].map(i => <span>{i/2}</span>)}

The above code will result in an error since you cannot divide a string by a number. The output, when using the preserve option, looks like:

var a = <div>  
  {['foo', 'bar'].map(function (i) { return <span>{i / 2}</span>; })}

React integration

JSX in TypeScript was specifically designed to be somewhat agnostic of the consumer. However, React will represent the majority of the usage. I gave a talk on integrating React and TypeScript earlier in the year. Many of the same principles still apply. The React DefiniteTyped repo was recently updated to integrate the concepts here like JSX.IntrinsicElements and JSX.ElementAttributesProperty.

/// <reference path="react.d.ts" />

interface Props {  
  foo: string;

class MyComponent extends React.Component<Props, {}> {  
  render() {
    return <span>{this.props.foo}</span>

<MyComponent foo="bar" />; // ok  
<MyComponent foo={0} />; // error  

Additional Resources

Most of the information contained in this post comes from issue #3203. However, this discussion has been ongoing for a long time and spread over multiple issues. I have compiled list if you're interested in deep-diving.