DSL digitado no TypeScript de JSX


O TypeScript possui suporte interno à sintaxe JSX e o compilador TypeScript fornece ferramentas úteis para configurar o processo de compilação JSX. Essencialmente, isso torna possível escrever DSL digitada usando JSX. Este artigo discutirá exatamente isso - como escrever uma DSL de r usando jsx. Interessado, peço gato.


Repositório com um exemplo pronto.


Neste artigo, não mostrarei as possibilidades com exemplos relacionados à Web, React e similares. Um exemplo não da Web demonstrará que os recursos do JSX não estão limitados ao React, seus componentes e a geração de html no caso geral. Neste artigo, mostrarei como implementar o DSL para gerar objetos de mensagem para o Slack .


Aqui está o código que usamos como base. Esta é uma pequena fábrica de mensagens do mesmo 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 ter uma boa aparência, mas há um ponto que pode ser significativamente aprimorado - legibilidade . Por exemplo, preste atenção à propriedade incompreensível da color , aos dois campos do título ( title e title_link ) ou aos sublinhados no text (o texto dentro de _ estará em itálico ). Tudo isso nos impede de separar o conteúdo dos detalhes estilísticos, dificultando a descoberta do que é importante. E com esses problemas, o DSL deve ajudar.


Aqui está o mesmo exemplo escrito apenas em 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> 

Muito melhor! Tudo o que deveria viver junto é unido, detalhes estilísticos e conteúdo são claramente separados - beleza em uma palavra.


Escrevendo uma DSL


Personalizar o projeto


Primeiro, você precisa ativar o JSX no projeto e informar ao compilador que não estamos usando o React, que nosso JSX precisa ser compilado de maneira diferente.


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

"jsx": "react" react "jsx": "react" inclui suporte a JSX no projeto e o compilador compila todos os elementos React.createElement nas chamadas React.createElement . E a opção "jsxFactory" configura o compilador para usar nossa fábrica de elementos JSX.


Após essas configurações simples, o código do formulário:


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

irá compilar em


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

Descrever tags JSX


Agora que o compilador sabe no que compilar o JSX, precisamos declarar as próprias tags. Para fazer isso, usamos um dos recursos interessantes do TypeScript - a saber, declarações de namespace local. Para o caso do JSX, o TypeScript espera que o projeto tenha um namespace JSX (o local específico do arquivo não importa) com a interface IntrinsicElements na qual as próprias tags são descritas. O compilador os captura e os usa para verificação de tipo e dicas.


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

Aqui declaramos todas as tags JSX para nossa DSL e todos os seus atributos. De fato, o nome da chave na interface é o nome da própria tag, que estará disponível no código. Valor é uma descrição dos atributos disponíveis. Algumas tags (no nosso caso) podem não ter atributos, outras podem ser opcionais ou até necessárias.


Fábrica propriamente dita - Template.create


Nossa fábrica de tsconfig.json é assunto de conversa. Será usado em tempo de execução para criar objetos.


No caso mais simples, pode ser algo como isto:


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

Marcas que adicionam apenas estilos ao texto são fáceis de escrever (no nosso caso): nossa fábrica simplesmente envolve o conteúdo da marca em uma string com _ nos dois lados. Os problemas começam com tags complexas. Na maioria das vezes, gastei com eles, procurando uma solução mais limpa. Qual é o problema real?


E é que o compilador imprime o tipo <message>Text</message> em any . O que não chegou nem perto de uma DSL digitada, bem, ok, a segunda parte do problema é que o tipo de todas as tags será um depois de passar pela fábrica - essa é uma limitação do próprio JSX (em React, todas as tags são convertidas em ReactElement).


Os genéricos vão ao resgate!


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

Apenas Element adicionado e agora o compilador produzirá todas as tags JSX para o tipo de Element . Esse também é o comportamento padrão do compilador - use JSX.Element como o tipo para todas as tags.


Nosso Element possui apenas um método comum - convertê-lo em um tipo de objeto de mensagem. Infelizmente, nem sempre funcionará, apenas na <message/> nível superior e isso ocorrerá com o tempo limite.


E sob o spoiler está a versão completa da nossa fábrica.


Próprio código de fábrica
 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) } } 

Repositório com um exemplo pronto.


Em vez de uma conclusão


Quando fiz essas experiências, a equipe do TypeScript só tinha uma compreensão do poder e das limitações do que eles fizeram com o JSX. Agora, seus recursos são ainda maiores e a fábrica pode ser mais limpa. Se você quiser vasculhar e melhorar o repositório com um exemplo - Bem-vindo com solicitações pull.

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


All Articles