2019-08-23

Create a JSX transpiler

JSX or JavaScript XML is not part of javascript but can be compiled to the language. It is mostly famous for being used with virtual DOM libraries like react. But it can be used for anything. The base for this experiment is WTF is JSX by Jason Miller, the creator of preact. That is the only introduction to JSX you will ever need. This article focuses on using it with typescript (he uses babel) in order to get type checking. The main take away is that the following JSX:

/** @jsx h */
let foo = <div id="foo">Hello!</div>

transpiles into:

var foo = h('div', {id:"foo"}, 'Hello!')

Creating a JSX transpiler is a matter of defining h.

/** @jsx h */, the "pragma", tells the compiler which h function to use.

Setup

Creating a test.tsx file:

const jsx = <text attribute="foo">bar</text>

Typescript complains: Cannot use JSX unless the '--jsx' flag is provided. We can set "jsx": "preserve" in tsconfig.json, to make the error disappear. But if we run the compiler, npx tsc, the resulting test.jsx looks the same. That is what "preserve" does, typescript just creates a .jsx file.

According to the docs, other possible values for "jsx" are "react" and "react-native". Using the first, typescript has a new complaint: Cannot find name 'React'. With the pragma, typescript says: Cannot find name 'h' but it does create test.js:

"use strict";
/** @jsx h */
var jsx = h("text", { attribute: "foo" }, "bar");

Looks good. Time to create h.ts:

export default (...args) => console.log(args)

Importing it into test.tsx under the pragma, the compiler is happy. If we run npx ts-node test.tsx, it logs:

[ "text", { "attribute": "foo" }, "bar" ]

Now all you have to do is make h do something more useful than logging.

JSX types

When I said that typescript is not complaining, that was because I had "noImplicitAny": false in tsconfig.json. We need to set that to true.

Now we get a JSX element implicitly has type 'any' because no interface 'JSX.IntrinsicElements' exists error. Let's create jsx.d.ts:

declare namespace JSX {
  interface IntrinsicElements {
    [key: string]: any
  }
}

The compiler is happy. But this does not give us any type checking because we are saying that any element goes with any property ([key: string]: any).

If we only allow the element <text> with a required property attribute:

declare namespace JSX {
  interface IntrinsicElements {
    text: {
      attribute: string
    }
  }
}
<text attribute="foo">bar</text>

is valid. But:

<text attribute={1}>bar</text>

gets a Type 'number' is not assignable to type 'string' error.

<text>bar</text>

Property 'attribute' is missing in type '{}' but required in type '{ attribute: string; }'

<hello>World</hello>

Property 'hello' does not exist on type 'JSX.IntrinsicElements'

...and so on. We have proper type checking for our JSX.

Publishing with types

You have created a useful h function and the types for all possible base elements that it understands. Time to publish. I am not going to lie, this is the part that took me the most time. And gave me the biggest headache. The purpose of this post is to avoid going through this next time. And maybe save you a couple of hours.

I will not go through all my different failed attempts. I ended up having a look at the preact types for inspiration.

The solution was to not let typescript create the types automatically by setting "declaration" to false in tsconfig and write the types by hand.

First export the JSX namespace in jsx.d.ts as JSXInternal, like this:

export namespace JSXInternal {
  interface IntrinsicElements {
    text: {
      attribute: string
    }
  }
}

And create types.d.ts in the dist folder with:

export = myLibrary
export as namespace myLibrary

import { JSXInternal } from './jsx'

declare namespace myLibrary {
  export import JSX = JSXInternal
  interface Whatever { /* whatever your h returns */ }
  function h(data: any): Whatever
  namespace h {
    export import JSX = JSXInternal
  }
}

Don't forget to copy jsx.d.ts to the dist folder. And add "types": "dist/types.d.ts" to package.json.

You are ready to publish your JSX transpiler with type checking. I made pdfmakejsx.

Have fun.