TypeScriptの宣言空間とその不満
最近TypeScriptの型を触っていてハマったあたりのまとめ。だいたい仕様書に書いてあるとおりなので、すでに仕様書を読破している諸兄にはこの記事は必要ないです。
宣言空間 (declaration space) とは
宣言空間というのは、同一宣言空間で同じ名前が複数存在するとエラーになるような空間のこと。 TypeScriptには大きく分けて3つの宣言空間 (declaration space) がある *1。
どういうことかというと、TypeScriptでは次のコードがエラーにならない。
var M = 0; // 変数宣言空間 interface M {} // 型宣言空間 module M {} // 名前空間宣言空間
変数とinterfaceとmoduleは別々の宣言空間に宣言されるためコンフリクトしない。
純粋なJavaScriptでは1.の変数宣言空間しか存在しない。上のTypeScriptをコンパイルすると、
var M;
になることからも分かる。
クラスの宣言空間
TypeScriptのクラスは変数宣言空間と型宣言空間の両方に宣言される。
var M = 0; class M {} // Error: 変数宣言空間でコンフリクト
interface N {} class N {} // Error: 型宣言空間でコンフリクト
module L {} class L {} // OK: 名前空間宣言空間はコンフリクトしないのでエラーなし
2つのモジュール: instantiatedとnon-instantiated
ここまで「モジュールは名前空間宣言空間に宣言される」と書いてきたけど、これは non-instantiated モジュールに限った話。
non-instantiatedモジュールとは、簡単に言うとコンパイルしたら完全に消えてしまうモジュール。具体的には、interfaceまたは他のnon-instantiatedモジュールのみを含むモジュールのこと。
逆に言うと、変数、クラス、Enumなどを含むものは全てinstantiatedモジュールとなる。コンパイルしたらJSコードが生成されちゃうからね。
instantiatedモジュールは、名前空間宣言空間のほかに変数宣言空間にも宣言される。つまり、モジュールがinterface以外のメンバーを持つことで冒頭の例がエラーになる。
module M { var foo; // このメンバーによりMはinstantiatedモジュールになり変数宣言空間でも宣言される } var M = 0; // エラー:変数宣言空間でコンフリクト
これもコンパイル後のコードを見るとコンフリクトするのは分かりやすい。module Mが変数宣言空間(=JavaScriptの世界)で実体化 (instantiated) されるので、当然コンフリクトする。
var M; (function (M) { var foo; })(M || (M = {})); var M = 0; // Conflict!!!
=
で代入されるのは変数宣言空間のみ
さて、次のコードはb1の行は通るけどb2の行でエラーになる。
class A {} var B = A; var b1 = new B(); var b2: B; // Error: Could not find symbol 'B'.
B = A
でBにAを代入しているはずなのに、Bが見つからないと言われてしまう。
これは、普通の=
で代入されるのは変数宣言空間の変数のみであるため。
var b2: B
のBは型指定なので、型宣言空間でBを探すけど、それはコピーされてないので見つからずエラーになる。
つまり、クラスを宣言した場合、変数宣言空間にはコンストラクタが宣言され、型宣言空間に型が宣言されるということ。だからコンストラクタは=
で代入可能なのでnew B()
は成功してる。
モジュールの代入
ではモジュールはどうか?
Non-instantiatedモジュールの場合、代入のところでエラーになる。
module M {} var N = M; // Error: Could not find symbol 'M'.
理由はこれまで説明してきたとおり、non-instantiatedモジュールは名前空間型宣言だけに宣言されるため、変数宣言空間でMを探しても見つからないから。
Instantiatedモジュールの場合は変数宣言空間にも宣言されるので代入可能。
module M { class C {} } var N = M; var c = new N.C();
ところが、この代入したN
を使って型宣言はできない。
module M { class C {} } var N = M; var c: N.C; // Error: Could not find symbol 'C'.
理由は(もうクドイけど)=
による代入でコピーできるのは変数宣言空間だけなので、ここではC
のコンストラクタだけがコピーされたのであって型はコピーされてない。同様にモジュール内のInterfaceもコピーされない。
モジュールの完全なコピー(というかエイリアス)をやりたい人のために、TypeScriptでは専用のシンタックスが用意されている。var
の代わりにimport
を使うことで、モジュールとその内部の変数宣言空間、型宣言空間、名前空間型宣言をすべてコピーできる*4。
module M { class C {} } import N = M; // importで代入 var c: N.C = new N.C(); // 型指定もコンストラクタもOK!
モジュールなしでクラス単体の型宣言空間をコピーすることはたぶんできないと思う。
追記: id:vvakame先生より、interfaceを使えば型宣言空間のコピー相当のことができるとご指摘いただきました!
@teppeis http://t.co/FaPH524Jur という感じにやると型宣言空間のコピーをしたような気持ちになれます。
— わかめ@TypeScriptカッコガチ (@vvakame) 2014, 4月 24
型宣言空間と名前空間宣言空間の予約語
変数宣言空間にはECMAScriptの予約語があるけど、型宣言空間と名前空間宣言空間にはそれとは別に予約語がある。
予約語はPredefined Types*5と呼ばれる以下の5つ。
- any
- number
- boolean
- string
- void
なので、こういうのはエラーになる。
module foo.number {} // Error interface any {} // Error
逆にこういうのはエラーにならない。
var any = 1; // OK! 変数宣言空間では予約語ではない
ここで宣言されたのは変数宣言空間だけなので、型宣言空間のany
型は無傷でそのまま使える。よくできてますね。
あとおもしろいところとして、ECMAScriptの予約語は関係ないのでこういうのもエラーにならない。
module foo.return {}
ただしTypeScriptとしてはエラーにならないだけで、(instantiatedモジュールの場合の)生成後のJavaScriptのコードは当然動かないです。
不満
実はここまで前置きでここから本題なんだけども。
型宣言空間でPredefined Typeが使えないのは分かるけど、名前空間宣言空間で使えないのが問題。少なくとも名前空間のルート以外は使えてよいと思う。というか使いたい。
具体的に困っていることとして、既存のgoog.string
みたいなモジュールの型宣言ファイルが書けない。
declare module goog.string { // Error: Module name cannot be 'string'. }
importでは同じようにstring
は使えないし、=
による代入では前述したように通常の型宣言空間や名前空間宣言空間のコピーは不可能なので、現状回避策が見つかっていない。
例えばstringをstringsに置き換えてから変数stringをオブジェクト型で無理矢理作っても、
declare module goog.strings { export class Foo {} } declare module goog { export var string: { Foo: goog.strings.Foo } }
前述の理由によりnew goog.string.Foo()
はできるけどvar foo: goog.string.Foo
はできない。
つまりこうなる。
var foo: goog.strings.Foo = new goog.string.Foo();
これはキモイ。 型指定はコンパイル後に消えるから動くは動くのだが、何かもよおしてしまいそうなほどキモイ。
うーむ。