TypeScriptにpluginがやってくる 使ってみよう編


はじめに

どうも、 @Quramy です。
前回の投稿から随分日が経ってしまいましたが、この投稿はある意味で前回投稿の続編的な内容になります。

今日はTypeScript 2.3から導入されるLanguage Service Extensibilityと呼ばれている機能についてまとめてみようと思います1

どのような変更なのか

TypeScript Roadmapのリンクを辿っても、https://github.com/Microsoft/TypeScript/pull/12231 に行き着くだけで、パッと見は何の機能なのかよく分かりません。
このPRの実装を眺めると、次の機能が見えてきます。

  • tsconfig.jsonのcompilerOptionsに"plugins"というキーが追加されている
  • pluginsに指定した内容は、TypeScript本体からresolveされる

すなわち、tsconfig.jsonに以下のように記述しておくと、

tsconfig.json
{
  "compilerOptions": {
    "module": "commonjs",
    "target": "es5",
    "strict": true,
    "sourceMap": false,
    "plugins": [
      { "name": "hoge-plugin" },
      { "name": "foo-plugin" }
    ]
  }
}

TypeScriptがnode_modulesから、hoge-pluginやfoo-pluginをresolveしてrequireしてくれる、という意味です。

これまでのTypeScriptでは一貫して、TypeScript自体をユーザーに拡張させることを許してきませんでした。
このポリシー自体には賛否両論あるとは思いますが、ここにきてその頑なさが和らいだことになります。

pluginにできること・できないこと

さて、次に疑問に思うことは「pluginって言うけど何ができるようになるの?」ではないでしょうか。

transformerをイジれる訳じゃないのです

「pluggableな機構を持ったJavaScriptトランスパイラ」という文脈で頭をよぎるのは、やはりBabelではないでしょうか。
まず、誤解を生まないようにはっきりと断っておきますが、今回導入されるTypeScriptのpluginは、Babelにおけるpluginとは全くの別物です。

Babelにおけるpluginは、sourceとなるJavaScriptの抽象構文木(AST)を読み取り、必要に応じてASTを変換できます。
すなわち、トランスパイルそのものがBabel pluginの責務です。

一方、TypeScriptにもsourceに対応したASTを変換する機構としてtransformerが存在しています。
例えばReactの.tsxファイルに記述されたJSXの変換には、src/compiler/transformers/jsx.tsが用いられる、といった具合です。

しかし、今回紹介するTypeScript plugin機構はtransformerを拡張できる訳ではありません。
「pluginを使えばtscが生成するJavaScriptをイジれる!」とか思わないでください。
ソースコードの変換が行ないたければ、webpackのloaderやgulpのpluginを自分で作成するなりして頑張りましょう2

Language Serviceのpluginです

トランスパイルをカスタマイズできないのであれば、pluginでは一体何ができるというのでしょうか。
その答えは「Language Serviceの拡張」です。

TypeScriptのアーキテクチャにおけるLanguage Serviceの位置づけは、次の図が一番分かりやすいです。

https://github.com/Microsoft/TypeScript/wiki/Architectural-Overview#layer-overviewより

The "Language Service" exposes an additional layer around the core compiler pipeline that are best suiting editor-like applications.

とあるように、主にエディタに対して、種々の機能を提供するための存在です。
詳細は後述しますが、pluginではこのLanguage Serviceを自由に差し替えることができるものの、上図からもわかるとおり、StandaloneなCompiler(いわゆるtscコマンド)や、Core Compiler APIを変更できるわけではありません。

Language ServiceがどのようなAPIを備えているかを覗いてみると、例えば次のようなメソッドが列挙されています。
確かにエディタ向け、といった風合いのAPIが並んでいるのがわかります。

interface LanguageService {

  // 補完可能なキーワード候補の取得
  getCompletionsAtPosition(fileName: string, position: number): CompletionInfo;

  // QuickFixの取得
  getQuickInfoAtPosition(fileName: string, position: number): QuickInfo;

  // インデント幅の取得
  getIndentationAtPosition(fileName: string, position: number, options: EditorOptions | EditorSettings): number;
  // etc...
}

一部の例外はありますが、TypeScriptに対応したエディタ・IDEはさきほどの図中のtsserverを経由してLanguage Serviceにアクセスし、補完情報やエラー情報を取得するように実装されています。
したがって、Language Serviceのpluginを実装してしまえば、Emacs LispやVim scriptが書けなくても、これらのエディタに機能が追加できるのです。

背景

さて、今回のLanguage Service Pluginについて、登場の経緯等を少し書いておきたいと思います。
題して1分で知ったつもりになるLanguage Serviceの歴史。

元々は"TypeScript extensibility"(https://github.com/Microsoft/TypeScript/issues/6508)というissueとして、Roadmapに記載されていました。
すごーく雑に要約すると「TypeScript + Reactの構成で、JSXの構文チェックやpropsの補完をサポートしているんだから、Angular ComponentのTemplateもサポートしてあげたい!」という内容です(勿論他にも色々な論点があったのですけども)。

2016年の頭に作成されたこのissueはしばらくの間塩漬け状態だったのですが、Angular v2とともに整備されたAoT CompilerなどのTemplateの静的解析機構によって状況が動きました。
vscode-ng-language-serviceというVSC 向けのpluginが生みだされ、このpluginのrefactoringを行う過程でLanguage Serviceの拡張方法が整理されて冒頭のPRに収束しました。

より詳細な流れは下記あたりを追うと把握できると思います。

使ってみよう

ここからはpluginを導入すると何ができるかを、実例とともに紹介していきます。

Angular

まずはAngularのLanguage Service pluginです。
導入すると下記の機能が追加されます:

  • Componentのtemplateで補完が利用可能に
  • Componentのtemplateでエラーチェックが利用可能に
  • Componentのtemplateでツールチップ(SignatureHelp)が利用可能に

npmで普通にインストールできます。
@angularとあるように、Angularチーム謹製です。
Angular本体がJiT/AoT Compileに利用している機構が流用されており、コードを動作させるよりも早くtemplateのerrorに気付けるので開発が捗ります。

npm i @angular/language-service -D

tsconfig.jsonを次のように編集します。

tsconfig.json
{
  "compilerOptions": {
    :
    "plugins": [
      { "name": "@angular/language-service" }
    ]
  }
}

実際に動作させるとこんな感じです。

「Componentには name というプロパティは定義されてねーぞ!」と怒られているキャプチャですね。
tsconfigの設定さえしておけば、VSCだろうとvimだろうとこの通りです。

Vue.js

続いては vue-ts-plugin です。
package名からも推測がつくとおり、Vue.js + TypeScript向けのpluginで、Vue.jsのSingle File Components開発を補助してくれます。
インストールすると、.vueファイルに対して次の機能が追加されます:

  • .vueファイルを扱えるようになる。言い換えると、import MyComponent from './my-component.vue' がresolveされる
  • .vue内の<script> セクション内のコードをTypeScriptとして扱う。コード中での補完や定義ジャンプが利用可能に

ちなみに、現状では<template>セクションや<style>セクションに対してのサポートは存在しないようです3

このプラグインの内部ではvue-template-compilerを利用して、<script>セクションのコードを抽出してLanguage Serviceで扱えるようにしています。
先のAngularが、.tsから参照されるtemplateに機能を付与していたのに対して、Vue.jsの場合は、.vueファイルからTypeScript部分を抜き出してエディタと統合しよう、というアプローチですね。
個人的には、TypeScript単体ではinvalidなファイルがあたかもvalidであるかのように見えてしまうのはあまり好きでは無いのですが、そこはフレームワーク毎の考え方もあるのでしょう。

tslint

最後に紹介するのは、tslint-language-service です。
tslintの名が示すとおり、Language Serviceのエラーチェック機能にtslintのチェック機構を追加してくれます。

なお、この記事を書いている時点(2017.04.03)ではnpmへのリリースがされていなかったので、手元で、git clone, tsc, npm linkを実行して利用しています4

余談ですが、vimではsyntasticというプラグインにtslint連携がかれこれ2年以上も前から存在しています。
ですので、わざわざLanguage Service Pluginとして導入せずともvim上でlint結果を確認できたのですが、バッファ保存毎にsystem経由でnodeを起動するような実装であったため、とても実用に耐えるレベルではありませんでした。
一方、Language Service Pluginの場合はtsserver上にnodeプロセスが常駐するため、レスポンスは段違いです5

おわりに

このエントリでは、TypeScript の Language Service Pluginについて概要を解説しました。

実際のpluginもいくつか紹介しました。というよりも、今回紹介した3つのpluginしか確認できなかった、というのが実状です6
Language Service好きとしては、plugin開発が盛り上がってくれるとよいなーと思っています。

このエントリを書き始めた当初は、pluginの自作方法についても記載しようかと考えていたのですが、「使いたい!」と「作りたい!」ではあまりにも想定読者が変わってしまいそうなので、作り方編を別途用意しました。興味がある方はこちらも是非。


  1. 実はv2.2.1から利用可能なのですが、公式には「2.3で導入される機能」という位置づけのため、本稿でもこれにならっています。 

  2. 2017.04.25追記 [TypeScript 2.3] custom transformer を利用して実行時に型情報を参照可能にするを用いることで、API直叩き限定ではあるものの、transformerの拡張ができるらしいです。ヤバい世界だ... 

  3. .vueにおけるtemplateやstyleセクションの自由度を考えると、TypeScriptのpluginとしてここに何か手を打つのは難しそうな予感。 

  4. https://github.com/angelozerr/tslint-language-service/issues/11 にRelease準備用のissueが立っているので、npm i tslint-language-service でインストールできる日もそう遠くなさそう。 

  5. tslintのAPI自体が、compiler core(ts.Program)を外部からセットできるため、既存のLanguage Server上で動作させるとより軽快、というのも理由の1つです。 

  6. 2017.04.20追記 @Hchan_mgn さんに https://github.com/HerringtonDarkholme/ts-css-plugin を紹介してもらいました。.cssをresolveするpluginです