vue.jpeg
Apr 12, 2020

そろそろ本気でVueのTemplate型チェックと向き合うことにする

こんにちは。最後に仕事でVueを書いてからそろそろ半年が経とうとしているAndyです。 (最近はReact, Go, Terraformあたりを触ってます)

さて近頃はα版のバージョンも12を数え、そろそろVue 3のβ版リリースも見えてきました。 自分は趣味のプロダクトも含めて4年ほどVueを利用していますが、コンパイラの機能が日に日に充実していくのを眺めるのはとても楽しいです。

今回はそんなVue 2から大幅に進化したvue-nextのエコシステムを踏まえて、改めてVue Templateの型チェックに挑んでいきたいと思います。

TL;DR

  • 今まではVue Templateの型チェックがしんどかった
  • Vue 3からコンパイラがsourcemapを吐いてくれるようになった
  • TypeScript Compiler APIと組み合わせてTemplateの型チェックができるツールを作ったよ!

Vue Templateと静的検査

Vue.jsは独自のTemplateシステムをもつJavaScript Frameworkです。

単一のコンポーネントファイル内に責務ごとのブロックを設けることでUI + ロジックを記述する方式は、見通しの面でメリットがあるだけでなくHTMLコミュニティやCSSコミュニティが培ってきたWebの資産をフルに活用できるという点で非常に優れたソリューションだと考えています。

(特にScoped CSSの仕組みは、既存のCSS設計のパラダイムを一切損なうことなくモダンなWebに求められるカプセル化を実現した美しい発明です)

ところがこの数年間でWeb業界の流れは大きく変化し、JSXやStyled Componentといった仕組みに代表されるように "Everything written in JS"な思想がメインストリームとなりつつあるように感じます。 最たるモチベーションとなっているのがやはりTypeScriptの流行をきっかけとした「型安全な世界への渇望」でしょう。

自分自身もTypeScriptの解析器や型検査器を用いたツールをいくつか開発しているため、この強力な型システムを用いてHTMLやCSSの世界も型安全に扱おうというモチベーションについては異論はありません。

2020年現在、残念なことにこの分野においてはVue.jsはAngularやReactといったフレームワークに対して後塵を拝すカタチとなっています。

独自Templateの型検査はやはりハードルが高く、TypeScript Compilerが解釈可能なAST(抽象構文木)との行き来はこれまで容易には実現できませんでした。 (唯一veturプロジェクトが一定の成果をあげているように思えます)

中でも多くのReactエンジニアの不満を買った点として、

  • Template expressionでコンポーネント内参照している値の型チェックが効かない
  • Child Componentのprops型を検知できない

の2つがあげられるでしょう。どちらの立場も分かる筆者としてはそのような状況を数年間に渡り忸怩たる思いで眺めてきました。

vue-next compilerとsourcemap

転機となったのはvue-nextでのCompiler APIの改善と型定義の拡充です。

新しいバージョンのVueからはそれまでvue-template-compilerというパッケージで公開されていた SFC -> render functionへの変換処理がCore packageに取り込まれており、コンパイルの実行時にsourcemapも吐き出せるようになりました。

vue-next: /packages/compiler-core/src/options.ts#L83

また、これまで Vue.extendという形式で定義していたコンポーネントのスクリプト部分に新たに defineComponentというメソッドが使われるようになり、利用できるAPIの機能だけでなく型推論の精度が飛躍的に向上しました。Child componentの propsの型推論もより簡単に行えるようになります。

今までも vue-template-compilerを用いて SFC Template -> render function -> TS ASTといった変換処理は行えたのですが、これらの変更によってTS上で検知した型エラーを元のVueのコードに変換する処理が容易に行えます!

(katashinさんのブログを見る限りveturは独自sourcemapで頑張ってそう...)

Templateを型チェックする仕組みを自作する

Vue Templateとrender functionの行き来が簡単にできるようになったことで、Vueのコードを静的に解析して遊ぶ手段が増えました。

今回はこれまでに何度か素振りしたTypeScript Compiler APIを組み合わせて、実際にTemplateの型チェックができるようなツールを自作してみます。

作ったもの

vue-type-audit

Embedded content: https://github.com/andoshin11/vue-type-audit

こういうコンポーネントがあったとすると

invalid component

型エラーの結果を該当コードと合わせて表示してくれる

error report

なにをやっているか

詳細についてはREADME#Architectureを参照してほしいのですが、

  1. TS Compilerが .vueファイルを読み込む際にrender functionにコンパイルした仮想 .tsファイルを返却する
  2. 仮想 .tsファイルの型検査精度を向上させるための型ヘルパーをinjectする
  3. 仮想 .ts上で型エラーが検知されたらsourcemapをもとに .vueファイルを復元する

といった主に3点の処理を行っています。

Vue SFC transformation

厳密にはVue SFCから型検査可能な仮想 .tsファイルにたどり着くために計5回のコンパイルを行っているのですが、それぞれの目的について話すと長いので該当ソースを参照のこと。

Child componentのprops検査

本記事上部の例のようにTemplate内でChild componentを呼び出しているコードは、下記のL50のようなrender functionにコンパイルされます。

sample render function

_resolveComponent関数が返却した値が _createVNodeの第一引数に渡っていますが、実は _resolveComponentは汎用 Component型を返すためこのままではprops(第二引数の値)の型検査が行えません。

そこで今回は下記のような型ヘルパーをコード解析時に生成することでChild componentの型推論を実現しました。

Custom Type Helper

ざっくりと解説すると、

  1. _resolveComponentについてdeclare functionを用いて引数で受け取ったリテラル型をそのまま返り値型とするようなシグネチャに書き換え

  2. 当該コンポーネントに紐づけられている外部コンポーネントの一覧から { [componentName: string]: ComponentType }となるような型ディクショナリを生成

  3. _createVNode型を拡張し、第一引数にChild component名のリテラル型を受け取ったら2で定義した型ディクショナリから対象のComponentTypeをlookupして、propsの型を反映するようなシグネチャに上書き

といった流れです。面倒ですね。

Vue compilerが生成したrender functionになるべく手を加えず型ヘルパーでトリッキーなことをしている理由としては、単純に追加のsourcemapの生成が大変だったからです笑

vue-type-auditの使い所と特徴

直近はCLIツールとしてCI等での型検査が主なユースケースです。 エラーリポート部分をpluggableにして、先日紹介したtypescript-error-reporter-actionのようにGitHub上に型チェックの結果を表示するのもすぐできそうかななんて思ったり。

実は同様の目的でveturチームからもVTI(Vetur Terminal Interface)というツールが出ているのですが、まだまだお互い機能が出揃っていないのでしばらく様子見かなと考えています。

またveturとは異なりvue-type-auditの大きな特徴として、

  1. Vue core以外の依存を持たないこと
  2. Document Registryを必要としないこと

の2点が挙げられます。

まず1についてですが、veturはVSCode向けの拡張ツールとして開発されている経緯からLanguage Server ProtocolやVSCodeのTextDocument型への依存を強く持っていたり、Templateのtokenizeにもeslint-parserを利用しているといった特徴があります。

対してvue-type-auditは設計方針としてなるべくdependenciesを持たないシンプルな作りにしてあります。(その分機能はveturとは比較にならないほど少ないです)

理由としては純粋に実行環境(エディタ含む)についてアグノスティックなものにしたかったというものと、将来的にTypeScript Pluginとして提供することを念頭に置いていたからです。

Vueの開発支援ツールから派生したveturの型チェック機能に対して、TypeScriptのエコシステムに対するVueサポートを提供したいという思想の違いですね。

2についても大部分が1と同じ理由で、1つのVueファイルからTemplate・Script・Styleといった様々なRegionsを切り出してDocument Registryで取り回すveturに対して、vue-type-auditでは(内部ではオンメモリコンパイルを行っているものの)1つのVueファイルに対して1つの仮想 .tsファイルを返すよう徹底しています。

Regionごとに色付けや外部サービスにBridgeする必要があるveturとは異なり、こちらがファイルを投げつけるのはあくまでTypeScript Compilerだけなので、Compilerとのグルーとなるレイヤ(LS Host等)で極力resolverをシンプルにかけるような設計を意識したつもりです。

パフォーマンスに難があればどんどん内部に破壊的変更をいれていこうと思うのでフィードバックは絶賛募集中です。

(というか仕事でVueを書く機会が欲しすぎる...)

まとめ

  • TypeScript Compilerめっちゃ楽しい
  • render functionのバリエーションがややこしい
  • sourcemapの扱いは難しいけど使いこなせたらできることが広がりそう!