Typisiertes DSL in TypeScript von JSX


TypeScript bietet integrierte Unterstützung für die JSX-Syntax und der TypeScript-Compiler bietet nützliche Tools zum Einrichten des JSX-Kompilierungsprozesses. Dies ermöglicht im Wesentlichen das Schreiben von typisiertem DSL mit JSX. In diesem Artikel wird genau dies erläutert - wie man ein DSL schreibt von r mit jsx. Interessiert frage ich nach Katze.


Repository mit einem vorgefertigten Beispiel.


In diesem Artikel werde ich die Möglichkeiten nicht anhand von Beispielen für das Web, React und dergleichen zeigen. Ein Beispiel, das nicht aus dem Internet stammt, zeigt, dass die Funktionen von JSX nicht auf React, seine Komponenten und die Generierung von HTML im allgemeinen Fall beschränkt sind. In diesem Artikel werde ich zeigen, wie DSL implementiert wird, um Nachrichtenobjekte für Slack zu generieren.


Hier ist der Code, den wir als Grundlage nehmen. Dies ist eine kleine Nachrichtenfabrik des gleichen Typs:


interface Story { title: string link: string publishedAt: Date author: { name: string, avatarURL: string } } const template = (username: string, stories: Story[]) => ({ text: `:wave:  ${username},    .`, attachments: stories.map(s => ({ title, color: '#000000', title_link: s.link, author_name: s.author.name, author_icon: s.author.avatarURL, text: `  _${s.publishedAt}_.` }) }) 

Es scheint gut auszusehen, aber es gibt einen Punkt, der erheblich verbessert werden kann - die Lesbarkeit . title_link Sie beispielsweise auf die unverständliche color , die beiden Felder für den Titel ( title und title_link ) oder die Unterstriche im text (der Text in _ wird kursiv gedruckt ). All dies hindert uns daran, Inhalte von stilistischen Details zu trennen, was es schwierig macht, das Wichtige zu finden. Und bei solchen Problemen sollte DSL helfen.


Hier ist das gleiche Beispiel, das gerade in JSX geschrieben wurde:


 const template = (username: string, stories: Story[]) => <message> :wave:  ${username},    . {stories.map(s => <attachment color='#000000'> <author icon={s.author.avatarURL}>{s.author.name}</author> <title link={s.link}>{s.title}</title>   <i>{s.publishedAt}</i>. </attachment> )} </message> 

Viel besser! Alles, was zusammenleben soll, ist vereint, stilistische Details und Inhalte sind klar voneinander getrennt - Schönheit in einem Wort.


DSL schreiben


Passen Sie das Projekt an


Zuerst müssen Sie JSX im Projekt aktivieren und dem Compiler mitteilen, dass wir React nicht verwenden und dass unser JSX anders kompiliert werden muss.


 // tsconfig.json { "compilerOptions": { "jsx": "react", "jsxFactory": "Template.create" } } 

"jsx": "react" enthält JSX-Unterstützung im Projekt und der Compiler kompiliert alle JSX-Elemente in React.createElement Aufrufe. Und die Option "jsxFactory" konfiguriert den Compiler so, dass er unsere Factory von JSX-Elementen verwendet.


Nach diesen einfachen Einstellungen wird der Code des Formulars:


 import * as Template from './template' const JSX = <message>Text with <i>italic</i>.</message> 

wird in kompilieren


 const Template = require('./template'); const JSX = Template.create('message', null, 'Text with ', Template.create('i', null, 'italic'), '.'); 

Beschreiben von JSX-Tags


Nachdem der Compiler weiß, in was JSX kompiliert werden soll, müssen wir die Tags selbst deklarieren. Zu diesem Zweck verwenden wir eine der coolen Funktionen von TypeScript - nämlich lokale Namespace-Deklarationen. Für den Fall mit JSX erwartet TypeScript, dass das Projekt über einen JSX Namespace (der spezifische Speicherort der Datei spielt keine Rolle) mit der IntrinsicElements Schnittstelle verfügt, in der die Tags selbst beschrieben werden. Der Compiler fängt sie ab und verwendet sie zur Typprüfung und für Hinweise.


 // jsx.d.ts declare namespace JSX { interface IntrinsicElements { i: {} message: {} author: { icon: string } title: { link?: string } attachment: { color?: string } } } 

Hier haben wir alle JSX-Tags für unser DSL und alle ihre Attribute deklariert. Tatsächlich ist der Schlüsselname in der Schnittstelle der Name des Tags selbst, das im Code verfügbar sein wird. Wert ist eine Beschreibung der verfügbaren Attribute. Einige Tags (in unserem Fall i ) haben möglicherweise keine Attribute, andere sind möglicherweise optional oder sogar erforderlich.


Fabrik selbst - Template.create


Unsere Fabrik von tsconfig.json ist Gegenstand von Gesprächen. Es wird zur Laufzeit zum Erstellen von Objekten verwendet.


Im einfachsten Fall könnte es ungefähr so ​​aussehen:


 type Kinds = keyof JSX.IntrinsicElements //    type Attrubute<K extends Kinds> = JSX.IntrinsicElements[K] //    export const create = <K extends Kinds>(kind: K, attributes: Attrubute<K>, ...children) => { switch (kind) { case 'i': return `_${chidlren.join('')}_` default: // ... } } 

Tags, die dem Text nur Stile hinzufügen, sind einfach zu schreiben (in unserem Fall i ): Unsere Factory verpackt den Inhalt des Tags einfach in eine Zeichenfolge mit _ auf beiden Seiten. Probleme beginnen mit komplexen Tags. Die meiste Zeit habe ich mit ihnen verbracht und nach einer saubereren Lösung gesucht. Was ist das eigentliche Problem?


Und es ist so, dass der Compiler den <message>Text</message> an einen any druckt. Was mit einem typisierten DSL nicht in der Nähe war, na ja, okay, der zweite Teil des Problems besteht darin, dass der Typ aller Tags nach dem Durchlaufen der Factory eins ist - dies ist eine Einschränkung von JSX selbst (in React werden alle Tags in ReactElement konvertiert).


Generika helfen!


 // jsx.d.ts declare namespace JSX { interface Element { toMessage(): { text?: string attachments?: { text?: string author_name?: string author_icon?: string title_link?: string color?: string }[] } } interface IntrinsicElements { i: {} message: {} author: { icon: string } title: { link?: string } attachment: { color?: string } } } 

Es wurde nur Element hinzugefügt, und jetzt gibt der Compiler alle JSX-Tags an den Element . Dies ist auch das Standardverhalten des Compilers. Verwenden Sie JSX.Element als Typ für alle Tags.


Unser Element hat nur eine gemeinsame Methode - das Umwandeln in einen Nachrichtenobjekttyp. Leider funktioniert es nicht immer, nur auf dem <message/> der obersten Ebene, und dies ist eine Zeitüberschreitung.


Und unter dem Spoiler befindet sich die Vollversion unserer Fabrik.


Werkscode selbst
 import { flatten } from 'lodash' type Kinds = keyof JSX.IntrinsicElements //    type Attrubute<K extends Kinds> = JSX.IntrinsicElements[K] //    const isElement = (e: any): e is Element<any> => e && e.kind const is = <K extends Kinds>(k: K, e: string | Element<any>): e is Element<K> => isElement(e) && e.kind === k /*         () */ const buildText = (e: Element<any>) => e.children.filter(i => !isElement(i)).join('') const buildTitle = (e: Element<'title'>) => ({ title: buildText(e), title_link: e.attributes.link }) const buildAuthor = (e: Element<'author'>) => ({ author_name: buildText(e), author_icon: e.attributes.icon }) const buildAttachment = (e: Element<'attachment'>) => { const authorNode = e.children.find(i => is('author', i)) const author = authorNode ? buildAuthor(<Element<'author'>>authorNode) : {} const titleNode = e.children.find(i => is('title', i)) const title = titleNode ? buildTitle(<Element<'title'>>titleNode) : {} return { text: buildText(e), ...title, ...author, ...e.attributes } } class Element<K extends Kinds> { children: Array<string | Element<any>> constructor( public kind: K, public attributes: Attrubute<K>, children: Array<string | Element<any>> ) { this.children = flatten(children) } /* *          `<message/>` */ toMessage() { if (!is('message', this)) return {} const attachments = this.children.filter(i => is('attachment', i)).map(buildAttachment) return { attachments, text: buildText(this) } } } export const create = <K extends Kinds>(kind: K, attributes: Attrubute<K>, ...children) => { switch (kind) { case 'i': return `_${children.join('')}_` default: return new Element(kind, attributes, children) } } 

Repository mit einem vorgefertigten Beispiel.


Anstelle einer Schlussfolgerung


Als ich diese Experimente durchführte, hatte das TypeScript-Team nur ein Verständnis für die Leistungsfähigkeit und die Einschränkungen dessen, was sie mit JSX machten. Jetzt sind seine Fähigkeiten noch größer und die Fabrik kann sauberer geschrieben werden. Wenn Sie das Repository anhand eines Beispiels durchsuchen und verbessern möchten - Willkommen mit Pull-Anfragen.

Source: https://habr.com/ru/post/de433348/


All Articles