Slide 1

Slide 1 text

よくできたテンプレート言語として TypeScript + JSX を利用する試み 2024-11-16 TSKaigi Kansai 2024 @izumin5210

Slide 2

Slide 2 text

@izumin5210 ▶ バックエンドや Web フロントエンドを書きます - Go, TypeScript, React, GraphQL, Protobuf, Connect, … ▶ ISUCON に出るのが得意 ▶ 関西(明石)出身 ▶ 好きな TypeScript 5.6 は typescript.preferences.autoImportSpecifierExcludeRegexes LayerX, ex-Wantedly

Slide 3

Slide 3 text

今日話すこと ▶ Web Frontend 以外でもテンプレート言語の代わりに JSX を使うことについて ▶ 実際に JSX を Web Frontend 以外で利用する例とそのメリット ▶ おまけ: TypeScript 以外からも JSX を利用したプログラムに依存する方法の例

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

テンプレート言語 #とは

Slide 6

Slide 6 text

テンプレート言語 #とは ▶ HTML を返すサーバを書くときによく使うやつ(雑) 一般に テンプレート言語(Template Language) と呼ばれるものについて {{! handlebars }}

Hello {{ user.name }}!

<%# erb(Ruby), ejs(JavaScript) %>

Hello <%= user.name %>!

{{/* Go の text/template */}}

Hello {{ .User.Name }}!

Slide 7

Slide 7 text

テンプレート言語 #とは ▶ HTML - Web ブラウザに返すもの - メール本文 ▶ 長い文字列を組み立てたいとき - なんらかの 3rd party API に渡す JSON とか? - 長い文字列のうちで制御したい部分の割合が小さいなら、 テンプレート化してコードから切り離すモチベーションが生まれることがある 例えば、こういうときに使われる

Slide 8

Slide 8 text

テンプレート言語の課題 ▶ 開発支援が弱いことが多い - 静的型付け, 補完, Syntax Highlighting, etc. - テンプレートは「引数をもとにして文字列を生成する関数」ともいえ、 その引数に静的型付けができないのはつらい… {{! `user.` までいれたら `name` が補完されてほしい }} {{! 存在しないプロパティが指定されたら型エラーで怒ってほしい }}

Hello {{ user.name }}!

Slide 9

Slide 9 text

テンプレート言語の課題 ▶ そのテンプレート言語自体を覚えないといけない - 分岐・ループや別テンプレートの差し込みなどで、独特な構文や仕組みが顔を出す - 特に差し込みについては心理的なハードル(e.g. 「めんどくさい…」という気持ち)が、 テンプレートのリファクタの心理的ハードルに直結する {{! テンプレート言語の数だけ `if` の書き方がある }} {{#if user}}

Hello {{ user.name }}!

{{/if}} {{! テンプレート言語の数だけ partial の読み込み方がある }} {{> userHeader user=user }}

Slide 10

Slide 10 text

ところで、テンプレート言語と JSX は似たようなことをしてそう {{! handlebars }}

Hello {{ user.name }}!

// tsx(React) function Heading({ user }: Props) { return

Hello {user.name}!

; }

Slide 11

Slide 11 text

JSX を Web Frontend 以外でも使うことで、 テンプレート言語の課題を解消できないか

Slide 12

Slide 12 text

Web 以外での JSX その① - Email React Email https://react.email/ import { Html, Button } from "@react-email/components"; type Props = { /* ... */ } function Email({ user }: Props) { return ( Hello {user.name}! ); }

Slide 13

Slide 13 text

Web 以外での JSX その① - React Email Storybook のようなプレビュー UI がついてくる (これは JSX 関係ないんだけど、便利なので紹介)

Slide 14

Slide 14 text

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(); sendgrid.send({ /* ... */, html: emailHtml });

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

off topic ▶ なんだかんだ通知チャネルとしての “メール" は重要 ▶ なんだけど、メールってほんとに難しいんですよ… - 開発者に HTML/CSS を意識させたくないレベルでムズい - それをコンポーネントという形で HTML/CSS を隠せるのは本当にありがたい

Slide 17

Slide 17 text

話を戻す

Slide 18

Slide 18 text

▶ 普通の React (TypeScript + JSX) で書ける - 静的型付け!! 補完!!! - 通常のプログラミング言語と近いメンタルモデルで開発できるのでは? ▶ テンプレート言語よりは覚えやすい? - 人によっては書き慣れてるし、最終的にはただの JavaScript なので…?(個人差はあれど) 本当に? 覚えやすい? そもそも JSX って何?

Slide 19

Slide 19 text

あらためて、JSX とは

Slide 20

Slide 20 text

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)によって決定されるということを意味します。

Slide 21

Slide 21 text

// input

Hello {user.name}!

// output React.createElement("h1", null, "Hello ", user.name, "!");

Slide 22

Slide 22 text

// input // output React.createElement( "a", { href: speaker.githubUrl }, React.createElement( Image, { src: githubLogo, width: 24, height: 24 }, ), );

Slide 23

Slide 23 text

JSX #とは ▶ 最終的には普通の JavaScript のコード(Expression)になる - Syntax Sugar なため - 別の言語が組み込まれているのではなく、JavaScript になるだけ ▶ 「JavaScript である」とわかっていれば構文も理解しやすい(?) - e.g. Element 内に直接 if や for が書けないのは、最終的に Expression になるから JSX とは、JavaScript です(?)

Slide 24

Slide 24 text

テンプレート言語と TypeScript + JSX ▶ JSX の強み - 型や補完をはじめとした、TypeScript のためのエコシステムの恩恵を受けやすい - 抽象化・コンポーネント化などのプログラミングにおけるテクニックが TypeScript や React と同じように・気軽に活用できる ▶ 一方で、JSX を「テンプレート」とすると表現力が高すぎる説はある - 普通の TypeScript なので、複雑なロジックも書けてしまう - (View として使う場合に) View にロジックのせ過ぎでは…? みたいな問題は起こり得る - ルールと自制心は必要

Slide 25

Slide 25 text

「静的型付けで補完も効くテンプレート」 として TypeScript + JSX を使うイメージ、できてきました?

Slide 26

Slide 26 text

Q. HTML 以外には使えないの? A. やればできる

Slide 27

Slide 27 text

JSX をトランスパイルするとどうなるか

Slide 28

Slide 28 text

function Heading() { return ( ); }

Slide 29

Slide 29 text

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 }, ), ) ); }

Slide 30

Slide 30 text

👉 "jsx": "react" { "compilerOptions": { } } "jsx": "react-jsx" { "compilerOptions": { } }

Slide 31

Slide 31 text

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 }, ) }, ) ); }

Slide 32

Slide 32 text

"jsxImportSource": "@izumin5210/nanka-benri-na-yatsu", { "compilerOptions": { "jsx": "react-jsx", } }

Slide 33

Slide 33 text

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 }, ) }, ) ); }

Slide 34

Slide 34 text

JSX で HTML 以外を組み立てられるか ▶ トランスパイル先で使われる関数は任意の実装に差し替え可能 - "jsx": "react-jsx" と "jsxImportSource を設定するだけ - 他のトランスパイラにも似たようなオプションがある ▶ なので React 以外の実装に差し替えることも可能 ▶ だいたい何でもできそう

Slide 35

Slide 35 text

たとえば…? 「長く複雑な文字列を組み立てたいケース」…?

Slide 36

Slide 36 text

Slack

Slide 37

Slide 37 text

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` だけで書けるので、若干悪意ある例ではある

Slide 38

Slide 38 text

Web 以外での JSXその② - Slack(jsx-slack) ▶ Slack Block Kit のメッセージを JSX で記述できるようにするもの - こういう構造を json にすると複雑になるのはわかるが、とはいえ… - …という悩みを、JSX で記述できるようにして解決してくれる こんなかんじ https://github.com/yhatt/jsx-slack Hello, !

Slide 39

Slide 39 text

👉 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: "!" }, ], }, ], }, ], }; Hello, ! 情報密度が高まった (文字数や面積に対する意味のあるコードの割合が増えた) 印象ありません?

Slide 40

Slide 40 text

でかい JSON(構造化文字列)を JSX で記述するメリット ▶ 抽象化したコンポーネントにできることで、情報密度が高まる - もとの JSON も難しい実装ではないが、とはいえ文字数が多いと認知負荷は高い - 本当にやりたいのは「JSON を組み立てる」じゃなくて「通知メッセージを組み立てる」 こと 読み手も書き手もそれに集中させてくれるようなコードになる - もちろん JSX を使わず関数をうまく設計することで似たようなことはできるかもだが、 複雑なリッチテキストなどの要求がでても耐えうる設計をする難度は高い ▶ JSX は木構造の親子関係が見やすい(個人差ある?) - 同じく木構造をとるオブジェクトの記述がわかりやすくなりやすい - インデントと開始・終了タグにより視覚的な強弱がつきやすい

Slide 41

Slide 41 text

jsx-slack のように JSON 構造を吐き出すものがあるなら、 頑張ったら自作もできるということ

Slide 42

Slide 42 text

やってみた

Slide 43

Slide 43 text

https://github.com/izumin5210-sandbox/custom-jsx-runtimes

Slide 44

Slide 44 text

LINE

Slide 45

Slide 45 text

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 なオブジェクトはやっぱりこうなっちゃう

Slide 46

Slide 46 text

Web 以外での JSXその③ - 自作(LINE) https://github.com/izumin5210-sandbox/custom-jsx-runtimes の `./packages/jsx-line` にあります サンプルなのでちょっと `any` 漏れてるのは許してください function UserMention({ user }: { user: User }) { return ; } function Sample({ user }: { user: User }) { return ( Hello, ); } pushMessage({ message: , /* ... */ });

Slide 47

Slide 47 text

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 的な関数に食わせる形になりそう

Slide 48

Slide 48 text

できる気がしてきましたね? 「私が考えた最強の JSX runtime」を作ってみて、できたら教えてください!

Slide 49

Slide 49 text

(ところで自分は当面 LINE Bot を作る予定がないので、 jsx-line を求めている方いればぜひ挑戦してみてください)

Slide 50

Slide 50 text

Q. バックエンドが〇〇(お好きな言語を入れてください)だから JSX 使えない 🥺 A. 一応やりようはある

Slide 51

Slide 51 text

JSX で記述したテンプレートを他の言語から呼び出す ▶ 1: JSX から別言語のテンプレート(e.g. erb, Go の text/template )を生成する ▶ 2: JS 側もサーバにして、API や RPC として呼び出す - 2a: すごく単純に、JSX の render 結果を返す API にしてしまう - 2b: JSX の render 結果を利用するところまでを責務とするマイクロサービスにしてしまう ▶ (もうちょい悪い方法がありそうだけど、検証しきれなかった) 検討したもの

Slide 52

Slide 52 text

1: JSX から別言語のテンプレートを生成する ▶ JSX から text/template などの文字列を生成してファイルに書き出し、 それをバックエンドのプログラムから利用するイメージ ▶ 技術書展はそうなってるらしい - ひかる黄金わかめ帝国さんの『GoからHTMLメール React Email風味』を読んでください - https://techbookfest.org/product/75YKV8JanhH6Ku0uGvYj7H? productVariantID=hRQpJuWjvCKVqSMrKPQ6e8

Slide 53

Slide 53 text

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) {} }

Slide 54

Slide 54 text

2b: JSX の render 結果を利用するところまでを 責務とするマイクロサービスにしてしまう ▶ e.g. メール送信を責務とするサービス, Slack 通知を責務とするサービス - 特に通知系であれば重複排除や時間帯による送信制御などのロジックが必要になるので それもふくめて責務とするサービスに切り出してしまう message SendReminderMessageRequest { // JSX の props そのままくらいの勢い } message SendReminderMessageResponse {} service SlackService { rpc SendReminderMessage(SendReminderMessageRequest) returns (SendReminderMessageResponse) {} }

Slide 55

Slide 55 text

2: JS 側もサーバにして、API として呼び出す ▶ 2a, 2b いずれにせよ管理するサーバプロセスが増えるのは明確なデメリット - マルチサービス・マイクロサービス的なことをしてないのであればなおのこと ▶ 呼び出し元のサービスが限られているのであれば、 いっそ同一コンテナあるいはサイドカーとする手もある - 同一コンテナなら UNIX Domain Socket で繋げばネットワークのオーバーヘッドを減らせる - サンプル: github.com/izumin5210-sandbox/custom-jsx-runtimes

Slide 56

Slide 56 text

さらなるアーキテクチャ的な話 ▶ デカい構造化文字列を組み立てたいケースは View であることも多い - メールや Slack はバックエンドから送るのでバックエンドだと考えがちだが、 「エンドユーザ向けのコンテンツを組み立ててる」という点で見ると View といえる ▶ なので、View ロジックを分散させないために 「Web Frontend が叩く API をメール本文でも使う」は合理性がありそう - たとえば GraphQL クエリを投げる ▶ こういう話大好きだけど時間が足りないので、気になる人はお声がけください

Slide 57

Slide 57 text

まとめ

Slide 58

Slide 58 text

▶ 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 経由で利用する など