TypeScript поддерживает JSX, а компилятор TypeScript предоставляет действительно хорошие инструменты для настройки того, как будет компилироваться JSX и, в конечном итоге, дает возможность писать DSL поверх JSX, который будет проверяться типом во время компиляции. Эта статья как раз об этом - как реализовать DSL поверх JSX.

📦 Репозиторий с полным рабочим примером.

В качестве примера JSX DSL я не буду использовать ничего, связанного с Интернетом или React, чтобы дать вам понять, что TypeScript JSX никоим образом не ограничивается компонентами React или рендерингом. Я буду реализовывать DSL для шаблонов богатых сообщений Slack с проверкой типа.

Например, это шаблон сообщения Slack, созданный из объектов.

Выглядит нормально, но вот кое-что, что мы можем улучшить - удобочитаемость. Например, посмотрите на свойство color в приложениях или на title_link вместе с этими _ (курсивом в Slack) в text. Они вмешиваются в содержание и затрудняют различение того, что важно, а что нет. Наш шаблон DSL может решить эту проблему.

Следующий пример описывает точно такое же сообщение, но с DSL, который мы собираемся реализовать.

Второй пример намного лучше - четкое разделение контента и стиля.

🔮✨ Внедрение DSL

⚙️ Конфигурация проекта

Прежде всего, мы должны включить синтаксис JSX в проекте TypeScript и сообщить компилятору, что мы не используем React и нам нужно, чтобы JSX компилировался по-другому.

Опция "jsx": "react" включает поддержку синтаксиса JSX в проекте и компилирует все элементы JSX для вызовов React.createElement. Затем с помощью параметра "jsxFactory" мы сообщаем компилятору, что мы не используем React и что он нужен для компиляции тегов JSX для вызовов функции Template.create.

И сейчас

Примерно компилируется в

🏷 Теги JSX

Теперь компилятор знает, какие функции JavaScript вызывает синтаксис JSX, и пора фактически определить теги DSL.

Для этого воспользуемся действительно крутой особенностью TypeScript - определениями пространств имен внутри проекта. Фактически, нам нужно определить имя и атрибуты каждого тега JSX, и для этого мы должны определить пространство имен JSX с интерфейсом IntrinsicElements, а компилятор TypeScript и языковые службы будут выбирать их и использовать для проверки типов и автозаполнения.

Здесь мы определили все теги JSX из примера со всеми их атрибутами. Таким образом, имя ключа в интерфейсе - это фактическое имя тега, а правая часть - это определение его атрибутов. Обратите внимание, что у некоторых тегов нет атрибутов, таких как i и message, а у других есть необязательные атрибуты.

🛠 Template.create

Теперь пора определить фабричную функцию для тегов JSX. Помните тот Template.create из tsconfig.json? Пришло время его реализовать.

Ага, теперь у нас есть базовое определение Template.

Теги, которые просто добавляют стиль к тексту, например тег i, просты. Мы просто возвращаем их содержимое в виде строки, заключенной в _. Но с более сложными тегами не так очевидно, что делать. На самом деле большую часть времени я потратил на эту часть, пытаясь найти хорошее решение. Так в чем проблема?

Проблема в том, что компилятор TypeScript определяет тип <message>Text</message> как any. Что далеко от цели DSL с проверкой типа. И дело в том, что невозможно объяснить компилятору TypeScript тип результата для каждого тега из-за ограничений JSX в целом - все теги производят один и тот же тип (что работает для React - все равно React.Component).

Итак, решение, которое я придумал, действительно простое - описать какой-нибудь общий тип для каждого тега и использовать его в качестве промежуточного состояния. Хорошие новости: TypeScript позволяет определять тип, который будет использоваться для всех тегов.

Мы только что добавили Element тип, и TypeScript теперь определяет тип каждого блока JSX как Element. Это поведение по умолчанию компилятора TypeScript в пространстве имен JSX. Он использует интерфейс с именем Element как тип для каждого блока JSX.

Теперь мы можем вернуться к Template и завершить его реализацию, чтобы вернуть объект, соответствующий этому интерфейсу.

Вот и все, теперь мы можем скомпилировать пример, и он создаст правильный объект сообщения Slack.

Я почти уверен, что ребята из TypeScript не планировали использовать синтаксис JSX таким образом, но это кажется полезным или, по крайней мере, очень интересным для экспериментов.

📦 Репозиторий с полным рабочим примером.