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.