はじめに
どうも、 @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に以下のように記述しておくと、
{
"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を次のように編集します。
{
"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の自作方法についても記載しようかと考えていたのですが、「使いたい!」と「作りたい!」ではあまりにも想定読者が変わってしまいそうなので、作り方編を別途用意しました。興味がある方はこちらも是非。
-
実はv2.2.1から利用可能なのですが、公式には「2.3で導入される機能」という位置づけのため、本稿でもこれにならっています。 ↩
-
2017.04.25追記 [TypeScript 2.3] custom transformer を利用して実行時に型情報を参照可能にするを用いることで、API直叩き限定ではあるものの、transformerの拡張ができるらしいです。ヤバい世界だ... ↩
-
.vueにおけるtemplateやstyleセクションの自由度を考えると、TypeScriptのpluginとしてここに何か手を打つのは難しそうな予感。 ↩
-
https://github.com/angelozerr/tslint-language-service/issues/11 にRelease準備用のissueが立っているので、
npm i tslint-language-service
でインストールできる日もそう遠くなさそう。 ↩ -
tslintのAPI自体が、compiler core(
ts.Program
)を外部からセットできるため、既存のLanguage Server上で動作させるとより軽快、というのも理由の1つです。 ↩ -
2017.04.20追記 @Hchan_mgn さんに https://github.com/HerringtonDarkholme/ts-css-plugin を紹介してもらいました。.cssをresolveするpluginです ↩