「外部パッケージの型定義もインストールし推論できるTypeScript playgroundを作った」という題で登壇してきました+スライドの補足

 · 23 min read

“外部ライブラリもインストール・型解釈できる TypeScript playground を作った”という題で俺得フロントエンド (1) LT 会という勉強会で登壇してきました。スライドはこちらです。

外部ライブラリもインストール・型解釈できる TypeScript playground を作った

スライドの内容をそのまま書いても意味がないので詳しくはスライドを読んでいただきたいのですが、発表時間の都合・構成力不足で伝えきれなかった部分の補足・補完的なものを書きます。
この playground のより詳細な説明、内部で使ってる技術の説明、この playground に限らず汎用的に活用できそうな技術の話をします。

何を作ったの

TypeScript Playground | The unofficial playground for advanced TypeScript users

↑ を作りました。

どうやって使うの

「動かないじゃん!」と言われても悲しいので、これをきちんと伝えておきたいです。
ちゃんと使えるものにしたいので、何通りかのユースケース用にサンプルを共有し「なるほどこういうことができるのね」のイメージを掴んでいただきたいです。

デモをいくつか見ると気づくかもしれませんが、reactのように型定義のないパッケージはインストール不要で、代わりに@types/reactのインストールが必要です。react は型定義を配布していないため、@types のパッケージが必要になります。
一方でパッケージ自体が型定義を配布している query-string や Redux などのパッケージはそれ自体を入れる必要が有ります。

文字にするとややこしいんですが、「普通に開発するときにインストールすること」と同じように使えば動くはずです。

なんで作ったの

TypeScript の playground といえば、TypeScript 公式の playgroundがまっさきに思い浮かぶと思います。主な機能は以下のとおりです。

  • 1ファイルで完結するくらいの TypeScript の型チェック
  • TypeScript のコードを書いて、JavaScript にトランスパイル
  • トランスパイルされた JavaScript を実行して動作確認
  • tsconfig の一部オプションをトグル可能
  • 書いたコードを復元できる URL を生成し共有

“〜をする Conditional Type”みたいな、ちょっとしたクイズとしては十分に活用できますし、TypeScript のようなトランスパイル文化に親しみのない方が具体的にどのような JavaScript が生成されるのかを理解したり、実行して成功体験を少しずつ積んだりと、ひとまず TypeScript に触ってみるものとしてとてもいい作りだと思っています。

ただ、型にフォーカスしたときにやや物足りないと思うことがあります。具体的には以下の問題が挙げられます。

  • 型定義を提供している npm パッケージと絡めた型が書けない
  • tsconfig でいじれる項目に限りがある
  • JSX が使えない
  • 使ってる TypeScript のバージョンがわからない

対象ユーザと存在意義が違うので、中途半端に本家に混ぜてどっちつかずになるよりも、とことん自分がほしいものを追求した playground を作ってみよう(その後は実用レベルになってから考えよう)と思いたち制作に至りました。

技術的なところ

だいたいはスライドの方に書いたので、書き足りないところ・省略したことについて加筆します。

  • monaco-editor の API の調べ方
  • ブラウザで文字列の圧縮・解凍
  • ブラウザでコードフォーマット(prettier)
  • comlink-loader での WebWorker 化がすごい

monaco-editor の API の調べ方

トーク後に「monaco で〜をするにはどんな API・機能を利用すればいいのか、ドキュメントを見てもいまいちわからない」という話をもらったので補足しますが、特別なことはありません。
API ドキュメントを隅から隅まで読む、目的から Issue を探してコメントを追う、ソースコード読むなどして泥臭く可能性を探り、monaco 公式の playground にて実験して確証を得てます。

公式ドキュメントはこちらです。

Monaco Editor

公式 playground はこちらです。
ユースケースごとに boilerplate が用意されており、初期コードを書き換えてだんだん挙動を理解していく形になると思います。
なお monaco-editor の playground にはシェア機能がありません。付け足したい。

Monaco Editor Playground

TypeScript に絞って理解を深めるとっかかりを紹介すると、

How to use addExtraLib in Monaco with an external type definition - Stack Overflow

ブラウザで文字列の圧縮・解凍

公式 playground と同じく、クエリパラメータにアプリの状態(コードや tsconfig、依存パッケージとそのバージョン etc)をすべて載せて共有できるようにし、ページロード時にクエリパラメータから状態を復元する方式を取りました。
よほど長いコードを書かない限りは愚直に base64 エンコードするだけで十分なのですが、共有可能なデータ量を増やすためにブラウザだけで完結する圧縮・解凍を実現するpakoにたどり着きました。

API の互換性はありませんが、Node.js のzlibモジュールのような感覚で使用できます。
Brotli には対応してなかったため gzip を使用したところ、本アプリにおける圧縮率はだいたい 30%でした。無圧縮よりも 30%ほど多くのデータ(コード)をシェアできるようになります。

実際のコードはこのあたりです。
https://github.com/Leko/type-puzzle/blob/61097601b7b35f92b3f611e164731496ffe1d601/packages/playground/src/lib/share.ts

WebAssembly に port された Brotli 実装のwasm-brotliというモジュールも試してみました。圧縮率は 40%と高かったのですが、gzip でも十分用途に足りていると判断し、利用ユーザ数が多い pako を採用しました。

ブラウザでコードフォーマット(prettier)

prettier をブラウザで使うのは非常に簡単で、ドキュメントのとおり書くだけです。

Browser · Prettier

WebWorker のサンプルコードも紹介されておりいたれりつくせりなんですが、Off the main thread を実現するにあたって、素の WebWorker のコードを書くよりも、webpack のworker-loaderを使うよりも、Comlinkを経由して暗黙的に WebWorker を利用するよりも、全力で振り切ってcomlink-loaderを使うアプローチが面白かったので紹介します。

もはや黒魔術の域を超え闇の魔術ではないか? とすら思うのですが、GoogleChromeLabs が開発しているcomlink-loaderという Webpack loader があります。
Comlink を生で使っている間は WebWorker の存在を利用者が認識しないといけないのですが、comlink-loader によって WebWorker の存在がコードからほとんど消失します。

重たい処理の例として、こんなクラスがあったとします。
無駄にループを回してメインスレッドを固めます。引数と戻り値を適当に付けておきました。
この重い処理が例えば圧縮・解凍アルゴリズムだったり、prettier だったりするという前提で適宜重たそうな処理に読み替えてください。

export class SomeHeavyTask {
  async run(loopCount: number) {
    const startsAt = new Date()
    for (let i = 0; i < loopCount; i++);
    return new Date().getTime() - startsAt.getTime()
  }
}

このSomeHeavyTaskはこのように使います

import { SomeHeavyTask } from './some-heavy-task'

const task = new SomeHeavyTask()
task.run(1_000_000_000).then(spent => console.log(`spent: ${spent}ms`))

この処理を comlink-loader に書き換えるとこうなります。

import SomeHeavyTask from 'comlink-loader!./some-heavy-task'

new SomeHeavyTask()
  .then(task => task.run())
  .then(spent => console.log(`spent: ${spent}ms`))
  • コンストラクタが Promise<SomeHeavyTask> にかわる
  • WebWorker に渡せる値しか渡せない
  • WebWorker でできることしかできない(ex. DOM 操作はできない)

という制約はあり、挙動を理解してないとむしろハマりそうな気もしますが、かなり低コストで Off the main thread が実現できるので、カジュアルに重たい処理は worker に逃がすって戦略を取りやすくなります。
処理が Worker に分けた結果 Code splitting も効くので、「特定用途にしか使ってない、ファイルサイズ的にも処理内容的にもヘビーなライブラリ」は格好の移行対象です。

import ShareWorker from "comlink-loader!./lib/share";

import PrettierWorker from "comlink-loader!./lib/prettier";

余計なテンプレート、グルーコードを増やさず気軽に Off the main thread を推し進められる強い武器(諸刃かもしれない)でした。

さいごに

スライドにも書いてあるのですが、完成度を上げて本家 playgrond に還元したいと思っています。
私一人のユースケースでは品質的にもコンセプト的にも甘いと思うので、ぜひ使ってみてフィードバックをいただけると幸いです。

TypeScriptJavaScriptnpmWebWorkercomlink
© 2012-2022 Leko