TypeScript has support for JSX and TypeScript’s compiler provides really nice tools to customize how JSX will be compiled and ultimately gives the ability to write DSL over JSX that will be type-checked on compile time. This article is exactly about that — how to implement a DSL over JSX.
As an example of a JSX DSL, I won’t use anything related to web or React to give you a hint that TypeScript’s JSX is not limited anyhow to React components or rendering. I’ll implement DSL for type-checked rich Slack message templates.
For example, this is a Slack message template constructed with objects.
It looks OK, but here is a thing that we can improve — readability. For example, take a look at color
property in the attachments or title_link
along with those _
(italics in Slack) in the text
. They mess with the content and make it harder to distinguish what’s important and what’s not. Our template DSL can solve this problem.
Next example describes exactly the same message but with the DSL we gonna implement.
Second example is much better — clear separation of content from styling.
🔮✨ Implementing the DSL
⚙️ Project config
First of all, we have to enable JSX syntax in TypeScript project and tell the compiler that we don’t use React and need JSX to be compiled differently.
Option "jsx": "react"
enables support for JSX syntax in the project and compiles all JSX elements to calls to React.createElement
. And then with the option "jsxFactory"
we tell the compiler that we don’t use React and need it to compile JSX tags to calls to Template.create
function.
And now
Roughly compiles to
🏷 JSX tags
Now compiler knows to what JavaScript function calls JSX syntax should be compiled and it’s time to actually define the DSL tags.
For this purpose, we will use really cool feature of TypeScript — in-project namespace definitions. In fact, we need to define name and attributes of each JSX tag and to do so we have to define namespace JSX
with interface IntrinsicElements
and TypeScript’s compiler and language services will pick them up and use for type-checking and autocompletion.
Here we defined all JSX tags from the example with all their attributes. So the name of a key in the interface is the actual name of the tag and right side is a definition of its attributes. Note that some tags don’t have any attributes like i
and message
while others have optional attributes.
🛠 Template.create
Now it’s time to define factory function for the JSX tags. Remember that Template.create
from the tsconfig.json
? Now it’s time to implement it.
Aha, now we have a basic definition of Template
.
Tags that just add styling to the text like i
tag are easy. We just return their content as string wrapped in _
. But with more complex tags it’s not that obvious what to do. In fact most of the time I did spend on this part — trying to come up with a good solution. So what’s the problem?
Problem right now is that TypeScript compiler infers type of <message>Text</message>
to be any
. Which isn’t close to the goal of type-checked DSL. And thing is, there is no way to explain to TypeScript compiler the type of result for each tag because of the limitation of JSX in general — all tags produce the same type(which works for React — everything is a React.Component
).
So the solution I came up is really simple — describe some common type for each tag and use it as intermediate state. Good news is TypeScript allows defining type that will be used for all tags.
We just added Element
type and TypeScript now infers the type of each JSX block to be of type Element
. This is a default behavior of TypeScript compiler on JSX namespace. It uses the interface with name Element
as a type for each JSX block.
Now we can go back to Template
and finish its implementation to return object matching this interface.
This is it, now we can compile the example and it will produce correct Slack message object.
I’m pretty sure TypeScript guys didn’t intent JSX syntax to be used this way, but this seems to be useful or at least very interesting to play around.