Mengetik DSL dalam TypeScript dari JSX


TypeScript memiliki dukungan bawaan untuk sintaks JSX dan kompiler TypeScript menyediakan alat yang berguna untuk mengatur proses kompilasi JSX. Pada dasarnya, ini memungkinkan untuk menulis DSL yang diketik menggunakan JSX. Artikel ini akan membahas persis ini - cara menulis DSL dari r menggunakan JSX. Tertarik, saya minta kucing.


Repositori dengan contoh yang sudah jadi.


Dalam artikel ini, saya tidak akan menunjukkan kemungkinan dengan contoh yang terkait dengan web, Bereaksi, dan sejenisnya. Contoh tidak dari web akan menunjukkan bahwa kemampuan JSX tidak terbatas pada Bereaksi, komponennya dan generasi html dalam kasus umum. Pada artikel ini, saya akan menunjukkan bagaimana menerapkan DSL untuk menghasilkan objek pesan untuk Slack .


Berikut adalah kode yang kami ambil sebagai dasar. Ini adalah pabrik pesan kecil dengan jenis yang sama:


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

Tampaknya terlihat bagus, tetapi ada satu hal yang dapat ditingkatkan secara signifikan - keterbacaan . Misalnya, perhatikan properti color tidak dapat dipahami, dua bidang untuk judul ( title dan title_link ), atau garis bawah dalam text (teks di dalam _ akan dicetak miring ). Semua ini mencegah kita untuk memisahkan konten dari detail gaya, sehingga sulit untuk menemukan apa yang penting. Dan dengan masalah seperti itu, DSL seharusnya membantu.


Berikut adalah contoh yang sama yang baru saja ditulis dalam 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> 

Jauh lebih baik! Segala sesuatu yang harus hidup bersama telah bersatu, detail gaya dan konten jelas dipisahkan - keindahan dalam satu kata.


Menulis DSL


Sesuaikan proyek


Pertama, Anda perlu mengaktifkan JSX dalam proyek dan memberi tahu kompiler bahwa kami tidak menggunakan Bereaksi, bahwa JSX kami perlu dikompilasi secara berbeda.


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

"jsx": "react" termasuk dukungan JSX dalam proyek dan kompiler mengkompilasi semua elemen JSX ke dalam panggilan React.createElement . Dan opsi "jsxFactory" mengkonfigurasi kompiler untuk menggunakan elemen JSX dari pabrik kami.


Setelah pengaturan sederhana ini, kode formulir:


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

akan dikompilasi


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

Jelaskan Tag JSX


Sekarang setelah kompiler tahu apa yang harus dikompilasi dengan JSX, kita perlu mendeklarasikan tag itu sendiri. Untuk melakukan ini, kami menggunakan salah satu fitur keren TypeScript - yaitu, deklarasi namespace lokal. Untuk kasus dengan JSX, TypeScript mengharapkan bahwa proyek memiliki namespace JSX (lokasi spesifik file tidak masalah) dengan antarmuka IntrinsicElements di mana tag dijelaskan. Compiler menangkap mereka dan menggunakannya untuk memeriksa tipe dan untuk petunjuk.


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

Di sini kami mendeklarasikan semua tag JSX untuk DSL kami dan semua atributnya. Bahkan, nama kunci di antarmuka adalah nama dari tag itu sendiri, yang akan tersedia dalam kode. Nilai adalah deskripsi atribut yang tersedia. Beberapa tag (dalam kasus kami) mungkin tidak memiliki atribut, yang lain mungkin opsional atau bahkan perlu.


Pabrik itu sendiri - Template.create


Pabrik kami dari tsconfig.json adalah topik pembicaraan. Ini akan digunakan dalam runtime untuk membuat objek.


Dalam kasus paling sederhana, mungkin terlihat seperti ini:


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

Tag yang hanya menambah gaya pada teks di dalamnya mudah untuk ditulis (dalam kasus kami): pabrik kami hanya membungkus isi tag dalam string dengan _ di kedua sisi. Masalah dimulai dengan tag kompleks. Sebagian besar waktu saya habiskan bersama mereka, mencari solusi yang lebih bersih. Apa masalah sebenarnya?


Dan kompiler mencetak tipe <message>Text</message> ke any . Yang tidak mendekati DSL yang diketik, yah, oke, bagian kedua dari masalahnya adalah bahwa jenis semua tag akan menjadi satu setelah melewati pabrik - ini adalah batasan JSX itu sendiri (dalam Bereaksi, semua tag dikonversi ke ReactElement).


Generik pergi untuk menyelamatkan!


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

Hanya Element ditambahkan dan sekarang kompiler akan menampilkan semua tag JSX ke tipe Element . Ini juga perilaku kompiler standar - gunakan JSX.Element sebagai jenis untuk semua tag.


Element kami hanya memiliki satu metode umum - casting ke tipe objek pesan. Sayangnya, ini tidak selalu berfungsi, hanya pada <message/> tingkat atas dan ini akan habis.


Dan di bawah spoiler adalah versi lengkap dari pabrik kami.


Kode pabrik itu sendiri
 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) } } 

Repositori dengan contoh yang sudah jadi.


Alih-alih sebuah kesimpulan


Ketika saya melakukan percobaan ini, tim TypeScript hanya memiliki pemahaman tentang kekuatan dan keterbatasan apa yang mereka lakukan dengan JSX. Sekarang kemampuannya bahkan lebih besar dan pabrik dapat ditulis bersih. Jika Anda ingin mencari-cari dan memperbaiki repositori dengan contoh - Selamat datang dengan permintaan tarik.

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


All Articles