Creating components with MithrilJS, Construct-UI, and TSX
A low-polygon rock studded with mithril ore

In case you hit this page due to a Google search for this error:

JSX element class does not support attributes because it does not have a __tsx_attrs property.

Feel free to just head to this section on the solution.

If you want further context, read on.

The Prologue

So, as I've been plugging away at Mnemosyne, I decided to incorporate a web admin panel. I use Ranvier for the game server, which stores all game data in flat files of YAML and JSON by default. These game files can be edited by hand to create new areas, items, NPCs, and so on. This is simple but also very error-prone, whereas a web UI allows for automating various parts of the building process and would make it easier for non-developers to contribute.

Using and upgrading zPanel

I decided on moving towards a more modern approach, using Andrew Zigler's zPanel as a starter and modifying it for my purposes rather than starting from scratch. The zPanel bundle uses MithrilJS, a JavaScript framework for creating SPAs that emphasizes performance, small download size, and more. I'll have a more detailed post eventually on some of the changes I've made to zPanel, but I first want to address my continued use of MithrilJS and one major hiccup I've ran into.

I was not familiar with MithrilJS at all before hacking away at zPanel, but opted to learn the framework's quirks rather than rewriting zPanel from the ground up. Despite making some heavy changes to zPanel on both the client and server side, I've chosen at each step to stick with MithrilJS, so I can vouch for it being dependable, flexible, and relatively easy-to-use.

Most recently, I've upgraded my edition of zPanel (which I've now been calling myelin-panel) to use Vite and TypeScript. Vite for the lightning-fast and low-configuration build tooling (compared to Webpack, at least), and TypeScript for the types. Alongside this was a conversion from JSX to TSX when writing my components, which is where I ran into some issues.

The Problem

You see, zPanel (and therefore myelin-panel) incorporate ConstructUI, a MithrilJS component library with nicely customizable and composable UI components but not very many frills. This includes a seeming lack of TypeScript support.

The __tsx_attrs property

When doing TSX in MithrilJS, I stumbled across some documentation indicating that types alone were not enough to support writing .tsx components in Mithril. I needed this library, mithril-tsx-component to provide an abstract class for me to extend and to define attributes that would be passed into my component's virtual DOM node (if you're unfamiliar with virtual DOM frameworks such as React, these attributes would be similar to regular HTML attributes, but I can define them and their types during development).

This mithril-tsx-component library uses a private property __tsx_attrs which is not publicly documented (to be fair, the library itself is archived, oops), and which is used to tell the TypeScript compiler which types are expected for each attribute. This is similar to React's PropTypes or the React FunctionComponent generic type. The use of this property is perfectly fine for writing my own class components, where I can define this property on my class that inherits from the abstract class:

/* This would expect an attribute "loading" which could be optionally passed in. If defined, foo would be a boolean. e.g. <EntityEditor loading /> would pass a VNode with an 'attrs' property of { loading: true } to the view function of this class. */ export default class EntityEditor extends MithrilTsxComponent<EntityEditorAttrs> { private __tsx_attrs { loading?: boolean; } }

(note: In fact, I think that the MithrilTsxComponent parent class would do this work for me)

However, using ConstructUI components caused the TypeScript compiler to barf with an error:

JSX element class does not support attributes because it does not have a __tsx_attrs property.

Some googling turned up plenty of examples of this without the __tsx_attrs property and with more generic property names such as props, but they all have the same cause.

The Root Cause

When using TypeScript and JSX/TSX, you can add definitions to the global JSX namespace. One of these interfaces you can define is called ElementAttributesProperty. This allows you to name the property that element types should have defined in order to have typed attributes.

The following snippet is from the mithril-tsx-component library:

declare global { namespace JSX { // Where to look for component type information interface ElementAttributesProperty { __tsx_attrs: any; } } }

This tells TypeScript that any element that is in a .tsx file (including imported .jsx elements) should have the __tsx_attrs property defined, and that it can be any type. Most important here is that the property is required.

This is what results in the error "JSX element class does not support attributes because it does not have a '__tsx_attrs' property."

The "Solution"

Alright, here is a hacky fix that got me started on my refactor. Expect a serious part two to this blog post.

Here's the fix in question:

declare global { namespace JSX { // Force JSX to ignore attributes. Needed for Construct-UI imports :( interface ElementAttributesProperty { ''?: any; } } }

This tells the TypeScript compiler that the ElementAttributesProperty can be an empty string (this is essentially a dummy value but prevents clashing with actual keys), and is optional, and of type any. Essentially, this tells the compiler to ignore the ElementAttributesProperty. This is a nuclear option that allowed me to continue to refactor my components using TypeScript, but that wipes out the usefulness of typed attrs between components (as far as I can tell). That said, TypeScript retains its usefulness in all other capacities.

Possible Follow-up

Some day I will get around to creating type definitions for the Construct-UI components, but for the time being I'm eager to get the web admin app for Mnemosyne back up and running. If I ever get around to it, expect a follow-up post and an open-source link to the type definitions.

In the meantime, I hope this article unblocks anyone else stuck facing a similar problem.