type-explorerがポシャった話

ポシャったので記録に残しておこうと思った回

なぜポシャったか

このコンセプトを解決するためのAPIがTypeScriptのCompilerAPIに存在しないであろうと結論づけたため

何が問題になったか

TypeParameterをもつ型を展開出来ないことがわかり、拡張機能として提供する 型を展開できる という機能が非常に限定的になってしまうことが問題となった

どういうことかというと、以下のような型 Generic<T> とそれを利用する Foo があったときに、

type Generic<T> = {
  prop: T
}
type Foo = Generic<{foo: string}>;

Fooの型が { prop: { foo: string; }} という形であることをCompilerAPIの操作で得ることが出来ない

これではConditionalTypeやMappedTypeを利用した型関数を使った結果がどんな型になっているか?を閲覧したいというモチベーションが解決できないという結論になった、という流れ

CompilerAPIの詳細

こちらの記事( type-explorerというTypeScriptの型を展開・閲覧出来るVSCode拡張を作っている - sisisinのブログ )で解説したとおり、この拡張機能では ts.Node 型を巡回、つまりTypeScriptのAST Nodeを見て回って型の構造を引っ張ってくるという処理をしている

さてこの仕組みでいくと、Genericsを持つ型はAST Node上では当然型パラメータを展開出来ないという課題とぶつかることになる

先の例でいえば、 Foo から以下のような構造を取りたいのだが、

type Foo
  +-- type Generic<{foo: string;}>
    +-- prop: {foo: string;}
      +-- foo: string

AST Node的には Foo 及び Generic<T> という2つの型の定義を独立にしか取ることが出来ないのだ

type Generic<T>
  +-- prop: T

---

type Foo
  +-- type Generic<{foo: string;}>

これは当然で、 AST Nodeはあくまで「テキストとして存在しているTypeScriptのコードを抽象構文木として扱う」役割でしかなく、そこに記述されている型情報を解決するのは別のComponentの役割だからだ

ではその型情報を解決するComponent(checkerというやつだ)のAPIを使えばいいだろうと軽く考えていたのだが、これが甘かった
確かに「型パラメータを解決してその結果を得る」事はできるのだが、その得られた結果がAST Nodeのように巡回可能な構造をしていなかった
checker.typeToString のような指定した型の解決した結果を文字列で得るというようなAPIしかなかったのだ

どうやらTypeScriptの型検査の仕組みは基本的に遅延評価になっていて、エディタサポートを提供するLanguageServiceでは指定されたNodeをピンポイントで解決してその結果を渡す、という構造になっているらしい(らしいというのはissueのコメントとかに書いてあったため)
これはパフォーマンス対策としてそうなっているとのことで、たしかにエディタサポートするのに都度プロジェクト全体を型検査していては重いのは想像しやすい

改めてissueを探してみると同じような課題を解決する方法を提供してくれ、という内容のものも見つかった

github.com github.com github.com

この辺のissueのやりとりで先のcheckerの仕組みに関する仕様について知ったあたりで、現状ある道具で素直に解決できる方法はなさそうだと思って諦めるに至ったのであった

他の方法はなかったのか

いくつか考えたことだけ列挙しておく

  • 拡張機能内でオンメモリのLanguageServiceHostを立てて置いて、VSCodeのTreeViewを操作するたびにそのHost内でファイルを操作してchecker.typeToStringの結果を得る
    • →同期が無理・Host内でのファイル操作を自明に解決する手段を作り上げるのが苦労しそう・ナイーブすぎてまともに動かすの苦しそう
  • checker.typeToStringの結果をparseしてAST Node化する
    • この記事を書いてて思いついたけど、typeToStringの結果がTypeScriptコードとして必ずしも解釈出来ないのでうーん。あとAST化してもNode単独ではgetTextを呼び出せない(SourceFile内にpos付きのNode、つまりTypeScriptプロジェクト内のファイル上にNodeが存在していないとgetTextを呼び出せず、Nodeを拡張機能上で表示できない)

おわりに

PoCしたらダメだったという感じなのでまあしょうがないかなぐらいの気持ちではあるけど、個人的にとても欲しかったのでその点は残念だった
(あとまあ世の中にないソリューションっぽかったからこれは出来たらかっちょいいなと思ってたフシもあった)(ソリューションがないのにはないなりの理由があると思い至れなかったのはアレ)