$30 off During Our Annual Pro Sale. View Details »

よくできたテンプレート言語として TypeScript + JSX を利用する試み / Usi...

よくできたテンプレート言語として TypeScript + JSX を利用する試み / Using TypeScript + JSX outside of Web Frontend #TSKaigiKansai

- TSKaigi Kansai 2024 発表資料
- https://kansai.tskaigi.org/talks/izumin5210
- サンプルリポジトリ: https://github.com/izumin5210-sandbox/custom-jsx-runtimes

Masayuki Izumi

November 15, 2024
Tweet

More Decks by Masayuki Izumi

Other Decks in Programming

Transcript

  1. @izumin5210 ▶ バックエンドや Web フロントエンドを書きます - Go, TypeScript, React, GraphQL,

    Protobuf, Connect, … ▶ ISUCON に出るのが得意 ▶ 関西(明石)出身 ▶ 好きな TypeScript 5.6 は typescript.preferences.autoImportSpecifierExcludeRegexes LayerX, ex-Wantedly
  2. 今日話すこと ▶ Web Frontend 以外でもテンプレート言語の代わりに JSX を使うことについて ▶ 実際に JSX

    を Web Frontend 以外で利用する例とそのメリット ▶ おまけ: TypeScript 以外からも JSX を利用したプログラムに依存する方法の例
  3. テンプレート言語 #とは ▶ HTML を返すサーバを書くときによく使うやつ(雑) 一般に テンプレート言語(Template Language) と呼ばれるものについて {{!

    handlebars }} <h1>Hello {{ user.name }}!</h1> <%# erb(Ruby), ejs(JavaScript) %> <h1>Hello <%= user.name %>!</h1> {{/* Go の text/template */}} <h1>Hello {{ .User.Name }}!</h1>
  4. テンプレート言語 #とは ▶ HTML - Web ブラウザに返すもの - メール本文 ▶

    長い文字列を組み立てたいとき - なんらかの 3rd party API に渡す JSON とか? - 長い文字列のうちで制御したい部分の割合が小さいなら、 テンプレート化してコードから切り離すモチベーションが生まれることがある 例えば、こういうときに使われる
  5. テンプレート言語の課題 ▶ 開発支援が弱いことが多い - 静的型付け, 補完, Syntax Highlighting, etc. -

    テンプレートは「引数をもとにして文字列を生成する関数」ともいえ、 その引数に静的型付けができないのはつらい… {{! `user.` までいれたら `name` が補完されてほしい }} {{! 存在しないプロパティが指定されたら型エラーで怒ってほしい }} <h1>Hello {{ user.name }}!</h1>
  6. Web 以外での JSX その① - Email React Email https://react.email/ import

    { Html, Button } from "@react-email/components"; type Props = { /* ... */ } function Email({ user }: Props) { return ( <Html lang="ja"> <Button href="https://example.com"> Hello {user.name}! </Button> </Html> ); }
  7. Web 以外での JSX その① - React Email Storybook のようなプレビュー UI

    がついてくる (これは JSX 関係ないんだけど、便利なので紹介)
  8. Web 以外での JSX その① - React Email 最終的に文字列化した HTML が得られるので、あとは煮るなり焼くなり

    // https://react.email/docs/integrations/sendgrid import { render } from "@react-email/components"; import sendgrid from "@sendgrid/mail"; import { Email } from "./email"; sendgrid.setApiKey(process.env.SENDGRID_API_KEY); const emailHtml = await render(<Email user={user} />); sendgrid.send({ /* ... */, html: emailHtml });
  9. Web 以外での JSX その① - React Email ▶ 普通の React

    (TypeScript + JSX) で書ける - 静的型付け!! 補完!!! - 通常のプログラミング言語と近いメンタルモデルで開発できるのでは? ▶ テンプレート言語よりは覚えやすい? - 人によっては書き慣れてるし、最終的にはただの JavaScript なので…?(個人差はあれど) ▶ 「コンポーネントを作っていく」考え方はメールと相性がいい - reusable な部品が多くなりがち、かつそもそもメールは難しいため抽象化の恩恵が大きい 何が嬉しい?
  10. off topic ▶ なんだかんだ通知チャネルとしての “メール" は重要 ▶ なんだけど、メールってほんとに難しいんですよ… - 開発者に

    HTML/CSS を意識させたくないレベルでムズい - それをコンポーネントという形で HTML/CSS を隠せるのは本当にありがたい
  11. ▶ 普通の React (TypeScript + JSX) で書ける - 静的型付け!! 補完!!!

    - 通常のプログラミング言語と近いメンタルモデルで開発できるのでは? ▶ テンプレート言語よりは覚えやすい? - 人によっては書き慣れてるし、最終的にはただの JavaScript なので…?(個人差はあれど) 本当に? 覚えやすい? そもそも JSX って何?
  12. JSX #とは ▶ JSX is an XML-like syntax extension to

    ECMAScript without any defined semantics. - https://facebook.github.io/jsx/ ▶ “without any defined semantics" - ChatGPT によると JSX自体には特定の 動作(セマンティクス) は定義されていません。 これは、JSXが単なる構文的な糖衣に過ぎず、実際の意味や挙動は使われるライブラリや フレームワーク(たとえばReact)によって決定されるということを意味します。
  13. // input <a href={speaker.githubUrl}> <Image src={githubLogo} width={24} height={24} /> </a>

    // output React.createElement( "a", { href: speaker.githubUrl }, React.createElement( Image, { src: githubLogo, width: 24, height: 24 }, ), );
  14. JSX #とは ▶ 最終的には普通の JavaScript のコード(Expression)になる - Syntax Sugar なため

    - 別の言語が組み込まれているのではなく、JavaScript になるだけ ▶ 「JavaScript である」とわかっていれば構文も理解しやすい(?) - e.g. Element 内に直接 if や for が書けないのは、最終的に Expression になるから JSX とは、JavaScript です(?)
  15. テンプレート言語と TypeScript + JSX ▶ JSX の強み - 型や補完をはじめとした、TypeScript のためのエコシステムの恩恵を受けやすい

    - 抽象化・コンポーネント化などのプログラミングにおけるテクニックが TypeScript や React と同じように・気軽に活用できる ▶ 一方で、JSX を「テンプレート」とすると表現力が高すぎる説はある - 普通の TypeScript なので、複雑なロジックも書けてしまう - (View として使う場合に) View にロジックのせ過ぎでは…? みたいな問題は起こり得る - ルールと自制心は必要
  16. tsconfig.json "jsx": "react" { "compilerOptions": { } } import Read

    from "react"; function Heading() { return ( React.createElement( "a", { href: speaker.githubUrl }, React.createElement( Image, { src: githubLogo, width: 24, height: 24 }, ), ) ); }
  17. import { jsx as _jsx } from "react/jsx-runtime"; function Heading({

    title }) { return ( _jsx( "a", { href: speaker.githubUrl, children: _jsx( Image, { src: githubLogo, width: 24, height: 24 }, ) }, ) ); }
  18. import { jsx as _jsx } from "@izumin5210/nanka-benri-na-yatsu/jsx-runtime"; _jsx( children:

    _jsx( function Heading({ title }) { return ( "a", { href: speaker.githubUrl, Image, { src: githubLogo, width: 24, height: 24 }, ) }, ) ); }
  19. JSX で HTML 以外を組み立てられるか ▶ トランスパイル先で使われる関数は任意の実装に差し替え可能 - "jsx": "react-jsx" と

    "jsxImportSource を設定するだけ - 他のトランスパイラにも似たようなオプションがある ▶ なので React 以外の実装に差し替えることも可能 ▶ だいたい何でもできそう
  20. const body = { blocks: [ { type: "rich_text", elements:

    [ { type: "rich_text_section", elements: [ { type: "text", text: "Hello " }, { type: "user", user_id: user.id }, { type: "text", text: "!" }, ], }, ], }, ], }; ※ これくらいなら `mrkdwn` だけで書けるので、若干悪意ある例ではある
  21. Web 以外での JSXその② - Slack(jsx-slack) ▶ Slack Block Kit のメッセージを

    JSX で記述できるようにするもの - こういう構造を json にすると複雑になるのはわかるが、とはいえ… - …という悩みを、JSX で記述できるようにして解決してくれる こんなかんじ https://github.com/yhatt/jsx-slack <Blocks> Hello, <a href={user.id}/>! </Blocks>
  22. 👉 const body = { blocks: [ { type: "rich_text",

    elements: [ { type: "rich_text_section", elements: [ { type: "text", text: "Hello " }, { type: "user", user_id: user.id }, { type: "text", text: "!" }, ], }, ], }, ], }; <Blocks> Hello, <a href={user.id}/>! </Blocks> 情報密度が高まった (文字数や面積に対する意味のあるコードの割合が増えた) 印象ありません?
  23. でかい JSON(構造化文字列)を JSX で記述するメリット ▶ 抽象化したコンポーネントにできることで、情報密度が高まる - もとの JSON も難しい実装ではないが、とはいえ文字数が多いと認知負荷は高い

    - 本当にやりたいのは「JSON を組み立てる」じゃなくて「通知メッセージを組み立てる」 こと 読み手も書き手もそれに集中させてくれるようなコードになる - もちろん JSX を使わず関数をうまく設計することで似たようなことはできるかもだが、 複雑なリッチテキストなどの要求がでても耐えうる設計をする難度は高い ▶ JSX は木構造の親子関係が見やすい(個人差ある?) - 同じく木構造をとるオブジェクトの記述がわかりやすくなりやすい - インデントと開始・終了タグにより視覚的な強弱がつきやすい
  24. const emojis = { laugh: { productId: "5ac1bfd5040ab15980c9b435", emojiId: "002"

    }, }; const body = { type: "textV2", text: "Hello, {5BAcrRd} {gwagReI}", substitution: { "5BAcrRd": { "type": "mention", "mentionee": { "type": "user", "userId": user.id }, }, "gwagReI": emojis.laugh, }, }; リッチテキストコンテンツのような、polymorphic なオブジェクトはやっぱりこうなっちゃう
  25. Web 以外での JSXその③ - 自作(LINE) https://github.com/izumin5210-sandbox/custom-jsx-runtimes の `./packages/jsx-line` にあります サンプルなのでちょっと

    `any` 漏れてるのは許してください function UserMention({ user }: { user: User }) { return <mention type="user" userId={user.id} />; } function Sample({ user }: { user: User }) { return ( <message> Hello, <UserMention user={user} /> <emoji name="laugh" /> </message> ); } pushMessage({ message: <Sample user={{ id: userId }} />, /* ... */ });
  26. JSX Runtime の自作 ▶ 型的な話は TypeScript の JSX のドキュメントに書いてある -

    JSX https://www.typescriptlang.org/docs/handbook/jsx.html ▶ 内部構造(React なら ReactElement )を決めて、それを返す jsx を実装する - TypeScript は JSX namespace の型をいい感じに見てくれる - JSX.IntrinsicElements をいい感じに書くことで、 独自の intrinsic element やその props を定義可能 ▶ 今回は jsx が雑に LINE Messaging API に渡せるものを返している - 複雑なものを作るときは中間表現を用意して、最後に render 的な関数に食わせる形になりそう
  27. JSX で記述したテンプレートを他の言語から呼び出す ▶ 1: JSX から別言語のテンプレート(e.g. erb, Go の text/template

    )を生成する ▶ 2: JS 側もサーバにして、API や RPC として呼び出す - 2a: すごく単純に、JSX の render 結果を返す API にしてしまう - 2b: JSX の render 結果を利用するところまでを責務とするマイクロサービスにしてしまう ▶ (もうちょい悪い方法がありそうだけど、検証しきれなかった) 検討したもの
  28. 1: JSX から別言語のテンプレートを生成する ▶ JSX から text/template などの文字列を生成してファイルに書き出し、 それをバックエンドのプログラムから利用するイメージ ▶

    技術書展はそうなってるらしい - ひかる黄金わかめ帝国さんの『GoからHTMLメール React Email風味』を読んでください - https://techbookfest.org/product/75YKV8JanhH6Ku0uGvYj7H? productVariantID=hRQpJuWjvCKVqSMrKPQ6e8
  29. 2a: JSX の render 結果を返す API や RPC ▶ 文字列組み立てのために

    I/O 発生させるのか? というのは悩ましいが… ▶ API スキーマを書くことで呼び出し側も型の恩恵を受けられるのはメリット gRPC あるいは Connect RPC を使う例 message CreateReminderMessageRequest { // JSX の props そのままくらいの勢い } message CreateReminderMessageResponse { string json = 1; } service SlackMessageBuilderService { rpc CreateReminderMessage(CreateReminderMessageRequest) returns (CreateReminderMessageResponse) {} }
  30. 2b: JSX の render 結果を利用するところまでを 責務とするマイクロサービスにしてしまう ▶ e.g. メール送信を責務とするサービス, Slack

    通知を責務とするサービス - 特に通知系であれば重複排除や時間帯による送信制御などのロジックが必要になるので それもふくめて責務とするサービスに切り出してしまう message SendReminderMessageRequest { // JSX の props そのままくらいの勢い } message SendReminderMessageResponse {} service SlackService { rpc SendReminderMessage(SendReminderMessageRequest) returns (SendReminderMessageResponse) {} }
  31. 2: JS 側もサーバにして、API として呼び出す ▶ 2a, 2b いずれにせよ管理するサーバプロセスが増えるのは明確なデメリット - マルチサービス・マイクロサービス的なことをしてないのであればなおのこと

    ▶ 呼び出し元のサービスが限られているのであれば、 いっそ同一コンテナあるいはサイドカーとする手もある - 同一コンテナなら UNIX Domain Socket で繋げばネットワークのオーバーヘッドを減らせる - サンプル: github.com/izumin5210-sandbox/custom-jsx-runtimes
  32. さらなるアーキテクチャ的な話 ▶ デカい構造化文字列を組み立てたいケースは View であることも多い - メールや Slack はバックエンドから送るのでバックエンドだと考えがちだが、 「エンドユーザ向けのコンテンツを組み立ててる」という点で見ると

    View といえる ▶ なので、View ロジックを分散させないために 「Web Frontend が叩く API をメール本文でも使う」は合理性がありそう - たとえば GraphQL クエリを投げる ▶ こういう話大好きだけど時間が足りないので、気になる人はお声がけください
  33. ▶ Web Frontend 以外でもテンプレート言語の代わりに JSX を使うことについて - JSX は単なる Syntax

    Sugar なので、通常の TypeScript と同じく強力な開発支援を受けられる - 抽象化・コンポーネント化などのプログラミングにおけるテクニックが React などと同じく気軽に活用できる ▶ 実際に JSX を Web Frontend 以外で利用する例とそのメリット - Email(React Email), Slack(jsx-slack), LINE(自作) - とくに大きな構造化文字列を組み立てるような実装では、 JSX を使うことで コードの情報密度を高められ、かつ視覚的な強弱がつきやすくなる - また、JSON を吐き出す JSX Runtime を自作するサンプルも紹介した ▶ TypeScript 以外からも JSX を利用したプログラムに依存する方法の例 - 別のテンプレート文字列を生成する, 別のサーバプロセスにして API 経由で利用する など