DSL mecanografiado en TypeScript de JSX


TypeScript tiene soporte incorporado para la sintaxis JSX y el compilador TypeScript proporciona herramientas útiles para configurar el proceso de compilación JSX. Esencialmente, esto hace posible escribir DSL mecanografiado usando JSX. Este artículo discutirá exactamente esto: cómo escribir un DSL de r usando jsx. Interesado, pido gato.


Repositorio con un ejemplo listo.


En este artículo, no mostraré las posibilidades con ejemplos relacionados con la web, React y similares. Un ejemplo que no sea de la web demostrará que las capacidades de JSX no se limitan a React, sus componentes y la generación de html en el caso general. En este artículo, mostraré cómo implementar DSL para generar objetos de mensaje para Slack .


Aquí está el código que tomamos como base. Esta es una pequeña fábrica de mensajes del mismo tipo:


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}_.` }) }) 

Parece que se ve bien, pero hay un punto que puede mejorarse significativamente: la legibilidad . Por ejemplo, preste atención a la propiedad de color incomprensible, los dos campos para el título ( title y title_link ) o los guiones bajos en el text (el texto dentro de _ estará en cursiva ). Todo esto nos impide separar el contenido de los detalles estilísticos, lo que dificulta encontrar lo que es importante. Y con tales problemas, DSL debería ayudar.


Aquí está el mismo ejemplo recién escrito en JSX:


 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> 

Mucho mejor! Todo lo que debe convivir tiene detalles y contenido unidos y estilísticos claramente separados: belleza en una palabra.


Escribir un DSL


Personaliza el proyecto


Primero debe habilitar JSX en el proyecto y decirle al compilador que no estamos utilizando React, que nuestro JSX debe compilarse de manera diferente.


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

"jsx": "react" incluye soporte JSX en el proyecto y el compilador compila todos los elementos JSX en llamadas React.createElement . Y la opción "jsxFactory" configura el compilador para usar nuestra fábrica de elementos JSX.


Después de estas configuraciones simples, el código del formulario:


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

se compilará en


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

Describir etiquetas JSX.


Ahora que el compilador sabe en qué compilar JSX, debemos declarar las etiquetas. Para hacer esto, utilizamos una de las características interesantes de TypeScript, a saber, las declaraciones de espacios de nombres locales. Para el caso de JSX, TypeScript espera que el proyecto tenga un espacio de nombres JSX (la ubicación específica del archivo no importa) con la interfaz IntrinsicElements en la que se describen las etiquetas. El compilador los atrapa y los utiliza para la verificación de tipos y para sugerencias.


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

Aquí declaramos todas las etiquetas JSX para nuestro DSL y todos sus atributos. De hecho, el nombre clave en la interfaz es el nombre de la etiqueta en sí, que estará disponible en el código. El valor es una descripción de los atributos disponibles. Algunas etiquetas ( i en nuestro caso) pueden no tener ningún atributo, otras pueden ser opcionales o incluso necesarias.


Fábrica en sí - Template.create


Nuestra fábrica de tsconfig.json es objeto de conversación. Se usará en tiempo de ejecución para crear objetos.


En el caso más simple, podría verse así:


 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: // ... } } 

Las etiquetas que agregan solo estilos al texto en el interior son fáciles de escribir ( i en nuestro caso): nuestra fábrica simplemente envuelve el contenido de la etiqueta en una cadena con _ en ambos lados. Los problemas comienzan con etiquetas complejas. La mayor parte del tiempo lo pasé con ellos, buscando una solución más limpia. ¿Cuál es el problema real?


Y es que el compilador imprime el tipo <message>Text</message> en any . Lo que no se acercó a un DSL escrito, bueno, está bien, la segunda parte del problema es que el tipo de todas las etiquetas será uno después de pasar por la fábrica; esta es una limitación de JSX (en React, todas las etiquetas se convierten a ReactElement).


¡Los genéricos van al rescate!


 // 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 } } } 

Solo Element agregó Element y ahora el compilador generará todas las etiquetas JSX en el tipo Element . Este también es el comportamiento estándar del compilador: use JSX.Element como tipo para todas las etiquetas.


Nuestro Element solo tiene un método común: convertirlo en un tipo de objeto de mensaje. Desafortunadamente, no siempre funcionará, solo en la <message/> nivel superior <message/> y esto estará en tiempo de espera.


Y debajo del spoiler está la versión completa de nuestra fábrica.


Código de fábrica en sí
 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) } } 

Repositorio con un ejemplo listo.


En lugar de una conclusión


Cuando realicé estos experimentos, el equipo de TypeScript solo entendió el poder y las limitaciones de lo que hicieron con JSX. Ahora sus capacidades son aún mayores y la fábrica puede ser más limpia. Si desea hurgar y mejorar el repositorio con un ejemplo: bienvenido con solicitudes de extracción.

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


All Articles