TypeScript – Declarationファイルを入手してJSライブラリを静的型付けする
TypeScriptのDeclarationファイルを公開しているGitHubリポジトリ
先日、REST APIのリファレンスを生成するフレームワークの記事を書いた際に、REST APIを叩くサンプルクライアントをTypeScriptで書いてみました。このサンプルではライブラリとしてjQueryとKnockoutを使ったのですが、JavaScriptで利用するときと同じように書くと、$などのキーワードが宣言されていないのでTypeScriptのコンパイルが通りません。jQueryの場合は$を、Knockoutの場合はkoをany型でアンビエント宣言してしまえばあとはいつも通りに書くだけでOKなのですが、せっかくなのでなるべく型付けして書くことにしました。
TypeScriptの場合、JavaScriptのライブラリを型付けして利用するには、Declarationファイルというインターフェースの宣言のみを記述したソースコードを用意する必要があります。jQueryはTypeScript公式のサンプルにDeclarationファイルがあるのでそれを使うことにしたものの、KnockoutはTypeScript公式にはDeclarationファイルがありません。自分でDeclarationファイルを書くのはやはり面倒だったのでWebで探したのですが、その際に大量のDeclarationファイルを公開しているGitHubのリポジトリを見つけました。
このリポジトリでは、JavaScriptライブラリのDeclarationファイルが100以上公開されています。以下はDeclarationファイルが公開されているフレームワークの例です。
- jQuery
- jQuery Mobile
- Underscore.js
- linq.js
- RxJS
- Backbone.js
- Knockout
- AngularJS
- Mustache
- Handlerbars.js
- RequireJS
- Jasmine
- QUnit
- EaselJS
- Three.js
- Node.js
- Express
他にもまだまだたくさんあります。私はJavaScriptをそんなにハードに使わないので、自分が使いそうなものはこのリポジトリだけでほとんどそろっていました。
ちなみに、Underscore.jsのDeclarationファイルは、any型が多用されているものと型がかなり厳密に定義されているものと2種類あるのですが、型が厳密な方のDeclarationファイルは3000行を超える超大作でした。こんなのとても自分で書けないのでとてもありがたいですね。
tsd
DefinitelyTypedのreadmeを読んでいたところ、tsdというTypeScriptのDeclarationファイルのパッケージマネージャが紹介されていました。tsdはコマンドラインツールで、DeclarationファイルをDefinitelyTypedなどのリポジトリから取得して配置してくれます。
tsdはnpmからインストールできます。
$ npm install tsd -g
使い方も簡単です。Declarationファイルをインストールしたいプロジェクトのディレクトリに移動し、installコマンドを使います。下の例はjQueryとKnockoutのDeclarationファイルをインストールします。
$ tsd install jquery knockout
デフォルトのインストール先は、./t.ds/[リポジトリ名]/[ライブラリ名]/の配下になります。インストール先を変更したい場合はncfgコマンドで設定ファイルを生成します。
$ tsd ncfg
これでtsd-config.jsonという名前の設定ファイルがカレントディレクトリに出力されますので、その中のlocalPathを変更してからinstallします。
また、allコマンドで利用可能なDeclarationファイルの一覧を見る事ができます。
$ tsd all
jQuery+Knockoutを型付けして使ってみた
別記事のサンプルアプリケーションでjQueryとKnockoutを使ったので、サンプルとしてのせておきます。
Knockout
以下のコードは、KnockoutのViewModelを書いている部分を抜き出してきたものです。
class ViewModel { posts: KnockoutObservableArray; name: KnockoutObservableString; comment: KnockoutObservableString; sendButtonEnable: KnockoutComputed; searchName: KnockoutObservableString; service: RestService; constructor() { this.posts = ko.observableArray() this.name = ko.observable() this.comment = ko.observable() this.sendButtonEnable = ko.computed(() => { return this.name() && this.name().length > 0 && this.comment() && this.comment().length > 0 }, this) this.searchName = ko.observable() this.service = new RestService("http://localhost:8080/posts") } sendButtonClickHandler(event) { this.service.addPost(this.name(), this.comment(), () => { this.clearPostInputs() this.executeSearch() }) } searchButtonClickHandler(event) { this.executeSearch() } executeSearch() { this.service.searchByName(this.searchName(), (posts) => this.posts(posts) ) } clearPostInputs() { this.name("") this.comment("") } }
koという変数が、Declarationファイル中でKnockoutStatic型としてアンビエント宣言されています。
knockout.d.ts
declare var ko: KnockoutStatic;
あとはkoを使っていつも通りobservable()やcomputed()でバインディングできます。配列のバインディングだけは専用のobservableArray()を使います。これは、配列専用のObservableオブジェクトを返すためです。
jQuery
jQueryはreadyとajaxしか使っていません。
$(() => { viewModel = new ViewModel() ko.applyBindings(viewModel) viewModel.executeSearch() }) ... addPost(name: string, comment: string, resultHandler: () => void) { var sendData = { "name" : name , "comment" : comment } $.ajax({ type: "POST", url: this.url, data: JSON.stringify(sendData), contentType: "application/json", dataType: "text" }) .done((data) => resultHandler()) .fail((jqHXR, textStatus, errorThrown) => console.log("addPost failed. " + textStatus + errorThrown)) }
jQueryも$がJQueryStaticという型でアンビエント宣言されていますので、これを使っていつも通りに書くだけです。
jquery.d.ts
declare var jQuery: JQueryStatic; declare var $: JQueryStatic;
なお、TypeScript公式で使っているjQueryのDeclarationファイルは、対応しているjQueryのバージョンが少し古いようです。Promiseまわりが対応できておらず、$.ajaxのショートハンドメソッドが返すjqXHRオブジェクトはdoneやfailが定義されていなかったため、自分で直して使っていました。その点、Difinitely TypedリポジトリのDeclarationファイルはきちんと対応されていましたので問題ありません。
余談ですが、ajaxメソッドの引数は一見any型の引数に見えますが、実はこのメソッドの引数はJQueryAjaxSettings型できちんと型が指定されています。TypeScriptの構造的部分型のおかげで連想配列を引数に渡す事ができています。ただし、JQueryAjaxSettings型のメンバは全てオプションになっているので、"type"を"typo"とタイポしたところでコンパイルは通ってしまって微妙です。でも、VisualStudioを使っていればコード補完が効くので、型の恩恵を受けることができます。
まとめ
静的型付けを持つAltJS系の言語を使う際に、既存のJavaScriptライブラリの型を解決してコンパイルできるようにするという問題は常につきまといます。TypeScriptに関してはany型を使えば基本なんとかなるとはいえ、型付けができるならそれに越した事はありません。こうやってDeclarationファイルを公開してもらえるのは本当にありがたいです。また、Declarationファイルを読むと、型が明記されている分フレームワークに対する理解を深めやすいです。
なお、実際にDeclarationファイルの内容を見てもらえれば分かりますが、メソッドを引数の型違いでオーバーロードしている箇所や型の概念が複雑になってしまう箇所はany型で宣言されていることが多いです。とはいえ、核となる部分に関してはきっちりと型が定義されているのでDeclarationファイルを使う意味はあると思います。TypeScriptのマイルストーンによればバージョン0.9でジェネリクスが実装されるということですが、もしメソッドの型パラメータが一緒にサポートされればもっと型付けをしやすくなりそうです。
それにしても、TypeScriptは公開から3ヶ月ちょっとでまだbetaにも関わらず、tsdのようなツールが作成されていたり、Web上の情報が豊富だったりと注目度の高さをうかがわせます。正式リリースはまだ先になりそうですが、引き続き追いかけていきたいと思います。