JSXのTypeScriptでの型付きDSL







TypeScriptにはJSX構文のサポートが組み込まれており、TypeScriptコンパイラーはJSXコンパイルプロセスをセットアップするための便利なツールを提供します。 基本的に、これにより、JSXを使用して型付きDSLを記述できます。 この記事では、これについて正確に説明します-DSLの書き方 から jsxを使用します。 猫の下で興味を持ってください。







既製の例を含むリポジトリ。







この記事では、Web、Reactなどに関連する例で可能性を示しません。 Web以外の例は、JSXの機能がReact、そのコンポーネント、一般的な場合のHTML生成に限定されないことを示します。 この記事では、DSLを実装してSlackのメッセージオブジェクトを生成する方法を示します。







基礎として使用するコードは次のとおりです。 これは同じタイプの小さなメッセージファクトリです。







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





見栄えはよさそうですが、大幅に改善できる点が1つあります。それは読みやすさです。 たとえば、わかりにくいcolor



プロパティ、タイトルの2つのフィールド( title



title_link



)、またはtext



の下線( _



内のテキストは斜体になります)に注意してください。 これにより、コンテンツをスタイルの詳細から分離できなくなり、重要なものを見つけることが難しくなります。 そして、このような問題があれば、DSLが役立つはずです。







これは、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>
      
      





はるかに良い! 一緒に住むべきものはすべて統一されており、スタイルの詳細と内容は明確に分離されています。つまり、美しさです。







DSLを書く



プロジェクトをカスタマイズする



まず、プロジェクトでJSXを有効にし、Reactを使用していないこと、JSXを別の方法でコンパイルする必要があることをコンパイラーに伝える必要があります。







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





"jsx": "react"



はプロジェクトのJSXサポートが含まれ、コンパイラーはすべてのJSX要素をReact.createElement



呼び出しにReact.createElement



ます。 また、 "jsxFactory"



オプションは、JSX要素のファクトリーを使用するようにコンパイラーを構成します。







これらの簡単な設定の後、フォームのコードは次のようになります。







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





コンパイルします







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





JSXタグの説明



コンパイラーがJSXをコンパイルする対象を認識したので、タグ自体を宣言する必要があります。 これを行うには、TypeScriptのクールな機能の1つ、つまりローカル名前空間宣言を使用します。 JSXの場合、TypeScriptは、プロジェクトにタグ自体が記述されているIntrinsicElements



インターフェイスを持つJSX



名前空間(ファイルの特定の場所は関係ありません)があることを想定しています。 コンパイラはそれらをキャッチし、型チェックおよびヒントに使用します。







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





ここでは、DSLのすべてのJSXタグとそのすべての属性を宣言しました。 実際、インターフェイスのキー名はタグ自体の名前であり、コードで使用できます。 値は、使用可能な属性の説明です。 一部のタグ(この例ではi



)は属性を持たない場合がありますが、その他のタグはオプションまたは必要です。







ファクトリー自体Template.create





tsconfig.json



ファクトリーが会話の対象です。 実行時にオブジェクトを作成するために使用されます。







最も単純な場合、次のようになります。







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





内部のテキストにスタイルのみを追加するタグは簡単に記述できます(この場合はi



):私たちのファクトリは、両側に_



を含む文字列でタグの内容を単純にラップします。 問題は複雑なタグで始まります。 ほとんどの時間、彼らと一緒に過ごし、よりクリーンなソリューションを探しました。 実際に問題は何ですか?







そして、コンパイラはtype <message>Text</message>



any



出力しany



。 型付きDSLには近づきませんでしたが、問題の2番目の部分は、工場を通過した後にすべてのタグのタイプが1つになることです。これはJSX自体の制限です(Reactでは、すべてのタグがReactElementに変換されます)







ジェネリック医薬品が助けになります!







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





Element



のみElement



追加され、コンパイラーはすべてのJSXタグをElement



タイプに出力します。 これも標準のコンパイラの動作です。すべてのタグのタイプとしてJSX.Element



を使用します。







Element



は、メッセージオブジェクトタイプにキャストする共通メソッドが1つしかありません。 残念ながら、常に機能するとは限らず、最上位の<message/>



でのみ機能し、タイムアウトになります。







そして、ネタバレは工場のフルバージョンです。







工場コード自体
 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) } }
      
      





既製の例を含むリポジトリ。







結論の代わりに



これらの実験を行ったとき、TypeScriptチームはJSXで行ったことのパワーと制限についてしか理解していませんでした。 現在、その機能はさらに大きくなり、工場はより簡潔に記述できます。 サンプルを探し回ってリポジトリを改善したい場合-プルリクエストでウェルカム。








All Articles