TypeScriptでライブラリを分離する方法

2016-06-25

TypeScriptでブラウザ用のコードを書く場合にWebPackなどでソースをまとめているんだけど、最終的に出力するJavaScriptのコードをライブラリとアプリで別にしたいといった場合にどうしたらいいのか、という方法。

具体的には、RxJSをライブラリとして使い場合に普通にimportを使って

import * as Rx from 'rxjs/Rx'
Rx.Observable.of(1, 2, 3)
.subscribe(x => console.log(x))

などというコードをtscでコンパイルすると

"use strict";
var Rx = require('rxjs/Rx');
Rx.Observable.of(1, 2, 3)
.subscribe(function (x) { return console.log(x); });

などとrxjs/Rxrequireするコードが出力され、WebPackによってRxJSと一体になったコードが出力される。

これをライブラリをグローバル変数Rxに格納することにして、アプリ側からは参照だけでrequireしないようにして、WebPackでまとめたアプリのコードにライブラリが結合されないようにしたい。

declare:anyを使う方法(型情報が失われる)

declareを使って、グローバル変数Rxanyで宣言してやる方法がある:

declare const Rx: any

一応これで実現できるのだが、コンパイル時に型情報が使われなくなってしまうので、せっかくのTypeScriptのメリットが薄れてしまう。

referenceを使う方法(デフォルトを参照できない?)

外部ファイルの型情報を取り込む方法に、referenceコメントを書く方法がある:

///<reference path="./node_modules/rxjs/Rx" />
declare const Rx: Rx // <= エラー

referenceで型情報を取り込めるが、Rxはデフォルトエクスポートでそれをreferenceで参照する方法がわからなかった…。

自分で定義ファイルを書く方法(面倒)

自分で.d.tsファイルを作成して、referenceで参照する方法もある:

// rxdef.d.ts
// 内容はテキトーです
interface Observable<T> {
of: (...args: T[]) => Observable<T>
subscribe: (x: T) => {}
}

interface Rx {
Observable: Observable<any>
}
// main.js
///<reference path="rxdef.d.ts" />

ただ、ライブラリの定義を自前で書くのは二度手間だし更新するのが大変になってしまう。

import文で型情報だけを使用する方法

TypeScriptで複数ファイル構成する2つの方法 - teppeis blogの「動的遅延ロード」の項目にrequireした外部モジュールの型をtypeofで使用する例が書いてあった。これを参考にしたところimportでも同様のことができた:

import * as IRx from 'rxjs/Rx'  // 念のため、名前をIRxと変更
declare const Rx: typeof IRx // typeofで型情報だけを使用する

外部モジュールをimportrequire)しても、実体を参照しなければtscでコンパイルした結果にはrequireされないことがわかったので、これで望みの動作が達成できる。

TypeScript…ちょっと難解です…。