TypeScriptで複数ファイルを扱おうとしてハマった点まとめ

2012/10/04 10:03 に Jun Shin が投稿   [ 2012/10/11 16:51 に更新しました ]
TypeScriptの紹介記事を書いた後、一日TypeScriptを触ってみて、主に「複数ファイルを管理する場合」についていろいろ試そうとして結構ハマったので、現時点までで分かった事をまとめておきます。あ、ハマったといっても、私の使い方がちょっと悪かったのと、用語の混乱があっただけで、基本はシンプルでしたよ!
もし理解が間違ってる部分があったらご指摘ください。m(__)m

先に結論だけ書いておきますと、

複数ファイルを扱う場合には、以下のように外部ファイルへの依存関係を指定しておく。
/// <reference path="hello.ts"/>

var h = new Hello("Jun" );
h.disp();

そんだけでした(^q^)

私がそれ以上に複雑なことをしようとして勝手にハマっただけですw

興味がある方は、以降を読んで、私が何をしようとして、どうハマったかを追体験してみてください。とりあえずnode.js環境入れて、複数ファイルに分けてプロジェクトを管理する流れを手探りで見つけていきます。

(1) とりあえず node.js 入れてみた

VisualStudio.NETは普段使わないので入れたくなくて、とりあえずnode.js入れてみました。
node.jsインストール後、コマンドラインから

npm install -g typescript

とすると、勝手にネットからTypeScriptを取ってきてnode.jsのプラグインとして設定してくれます(Macでも同じ手順のはず)。
それが完了すると、

tsc helloworld.ts

みたいな感じでTypeScriptファイル(.ts)をJavaScriptにコンパイルできるようになります。
tscコマンドにはいくつかオプションがあって(tscとだけ入力すると確認できます)、

tsc helloworld.ts other1.ts other2.ts --out all.js

とやると、全部の出力をall.js1つにまとめてくれたり、

tsc helloworld.ts -e

とやると、コンパイル後のjsをその場でnode.js環境で動かしてくれたりします。
(これ便利! console.log()に書いたものはその場でコンソールに出力されます)

事前にコマンドラインオプションをテキストファイルに書いておいて、

tsc @textfilename

とやると、便利かもしれません。

(2) Helloクラス作って動かした

何はともあれHello。神も確か「始めにHelloあれ」と言われたとかなんとか…。

hello.ts
class Hello {
    constructor( private name : string ){}
    disp(){ console.log( "Hello, " + this.name + "." ); }
}

var h = new Hello("Jun" );
h.disp();

tsc hello.ts -e

→hello.jsできて、コンソールにも「Hello, Jun.」って出た!ヾ(o´∀`o)ノ
いい感じいい感じ。

(2) せっかくなのでテストは別のファイルに分けて見た

やっぱ、クラスとそれのテストは別のファイルに分けておきたいわけです。本能的に。

hello.ts
class Hello {
    constructor( private name : string ){}
    disp(){ console.log( "Hello, " + this.name + "." ); }
}

testhello.ts
var h = new Hello("Jun" );
h.disp();

tsc testhello.ts

→「Helloが見つかりません!ヽ(`Д´)ノ

ですよねー。さすがに勝手にカレントディレクトリのtsファイルの中身まで見てくれませんよねー。

というわけで調べると、こんな感じで依存関係をコンパイラに教えるみたいです。

testhello.ts
/// <reference path="hello.ts"/>

var h = new Hello("Jun" );
h.disp();

tsc testhello.ts

→コンパイル通った!testhello.js と hello.js が正しく出力された!

「/// <reference path="hello.ts"/>」という部分に依存関係のあるファイルを指定するわけですね。なんか微妙にやっつけ仕様っぽく見えますが、きっとこんな表記になっているのには深遠な理由があるのでしょう…!

うまくいったので調子にのって「じゃあ実行だー!」と、以下を入力してみました。

tsc testhello.ts -e

→「Helloが見つかりません!ヽ(`Д´)ノ

え・・・。コンパイルは通るのに・・・。実行はしてくれないの・・・。

まぁでもよくよく考えると、実行環境のnode.jsは、testhello.jsだけ受け取ってるわけで、testhello.jsの中身を見ても、特に外部ファイルを読み込んでる処理は入ってないし、当然の結果。さきほどのTypeScript上のreference指定は、あくまでもコンパイル用の情報、という事でしょう。

そもそも、私はTypeScriptをブラウザ環境で動かすJavaScriptを生成する為に使おうとしているわけなので、JavaScript同士の依存関係については普通にHTML上のscriptタグでズラリと読み込んで解決しなきゃいけないのか、と気づきました。

なので、テストしたければ、次のようなhtmlファイルを作っておいてブラウザから開けばいいんですね。

test.html
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="utf-8" />
    <script src="hello.js"></script>
    <script src="testhello.js"></script>
    <title>TypeScript Test html</title>
</head>
<body>
</body>
</html>

もしくは、とりあえず動作テストをしたいだけなら、出力ファイルを1つにまとめてしまう、という手もあります。これなら複数ファイルの問題はなくなって、何も考えずにnode.js環境で実行できます。こんな感じで。

tsc testhello.ts --out all.js
node all.js

別にnode.js環境に関係なく、TypeScriptでは複数ファイルで管理して、JavaScriptではいくつかのファイルにマージする、というのはアリですよね。hogehogelib.js みたいな感じで。

(3) それはそれとして、外部スクリプトの動的ロード機能って用意されてないのか?

ていうか、TypeScriptのドキュメントファイル(PDF)読んでると、なんかimportっていう命令が出てくるんですよね…。

import lib = module( "lib" );

みたいな記述が。なんか、普通に外部スクリプトの動的ロードできそうな雰囲気しません?

詳しく読むと、ここで指定した"lib"は、このソースファイルのパスにlib.tsがあればそれを、それがなくてlib.d.tsがあればそれを依存関係として認識する、みたいな事が書かれているんですよ。

で、さらに読むと「export」というキーワードも出てくる…。

で、さっそくいろいろ試して見るんですが、とにかく駄目。main.ts 内で

import h = module( "hello" );

ってやっても、「現在のスコープにhelloなんてねーよ」と叱られます。ちゃんと同じフォルダにhello.tsあるのに! 何・・・どゆこと・・・!?

そこからいろいろ調べて、どうやらこのimportとexportは、CommonJS(またはAMD)moduleという仕様に合わせたJavaScriptコードを出力するためのものらしいことが分かりました。この辺の知識皆無だったもんで、混乱しまくり。

で、次のシンプルなサンプルを試してみると、ようやくうまく行きました。

log.ts
export function message( s: string ) {
console.log( s );
}

main.ts
import log = module("log");
log.message("hello");

tsc main.ts -e

→helloとコンソールに出力された!

出力されたJavaScriptファイルを見ると、こんな感じになってます。

log.js
function message(s) {
    console.log(s);
}
exports.message = message;

main.js
var log = require("./log")
log.message("hello");

どうやらCommonJSのmodule仕様というのは、「exportしたいオブジェクトを"exports"っていうオブジェクトの中に入れておいて、importしたい側では、そのファイル名を指定してrequireを呼び出せばOK!」って感じみたいです。内部ではファイルを非同期に読み込んで独立した名前空間の中でevalしてるようです。へーへーへー。

で、node.jsもCommonJSに準拠したrequireとか持ってるようなので、TypeScriptからexportとimport使うと、それに準拠した形でJavaScriptにしてくれて、node.jsから動的ロードが使える、という訳ですね。

なるほどなるほど。ちなみに、CommonJSではなくAMDという仕様もあるようで、TypeScriptでは tsc --module amd と指定すれば、それ用のJavaScriptコードを出力してくれるようです。(デフォルトはCommonJS)

(4) じゃあ最初のHelloクラスとテスト用コードをこれで書き直そう!

hello.ts
export class Hello {
greeting : string = "Hello";
constructor ( private name : string ){}
disp(){
console.log( this.greeting + ", " + this.name );
}
}

main.ts
/// <reference path="hello.ts"/>
import hello = module( "hello" );
var h = new hello.Hello( "jun1s" );
h.disp();

まぁ、うまく行ったんですけど…。あれぇ、これじゃ、Helloクラスが「hello」っていう名前空間の下でしか使えないじゃないか…。普通に new Hello( "jun1s" ) ってしたいのに…。

てかこれ、moduleで名前空間作った状態で、そのmodule内にHelloクラス定義してる場合はどうなるんだろう?

というわけでやってみました。

hello.ts
export module JunLib { // ←この行のexportはimportで読み込む為のもの
export class Hello { // ←この行のexportはJunLibの外にHelloを公開する為のもの
greeting : string = "Hello";
constructor ( private name : string ){}
disp(){
console.log( this.greeting + ", " + this.name );
}
}
}

main.ts
/// <reference path="hello.ts"/>
import hello = module( "hello" );
var h = new hello.JunLib.Hello( "jun1s" );
h.disp();

紆余曲折の結果、これでうまくいきました。ちなみにhello.tsのmoduleにちゃんと「export」つけないと、main.ts側で読み込めませんでした。

うまくいきましたけど、まったく納得行きません。

だってせっかくhello.tsでmodule使ってJunLibっていう名前空間作ってるのに、main.ts側で読み込んだら helllo.JunLib って感じで一段、名前空間が下がっちゃってるじゃないですか。いや、ありえないですこれ。多分私の使い方がなんかおかしい

それに、そもそもhello.tsで1行目の「export」と、2行目のclassについてる「export」の意味が違うんですよ。1行目のは、CommonJSのmodule仕様(つまり外部ファイルの動的ロード)の為のexportで、classについてるexportは、TypeScriptのmoduleのスコープ制御の為のexportなんです(ややこしいっ!!)。

あと、名前空間を作るためのmoduleキーワードと、CommonJS/AMDのmodule仕様の為のmoduleキーワードが、同じキーワードでごっちゃになります。(追記:どうもドキュメント読んでると、前者を「External Modules(外部モジュール)」、後者を「Internal Modules(内部モジュール)」と呼んで区別してるっぽい。CommonJS/AMDのmodule仕様に合わせた用語だろうけど、正直紛らわしい…)

まだ試してないのですが、名前空間のmoduleって、同じmodule名で複数のtsファイルに分割してもきちんとマージされるんですよ。でも、そうやって作った名前空間も、このやり方でimportするとバラバラになってしまう気がします。

というわけで、この辺でわけわかんなくなって調査終了。(追記:どうも内部モジュールと外部モジュールをごっちゃにしていたせいですね)

現時点での自分の理解の範囲内でまとめると、

  1. ファイルの依存関係は「/// <reference path="" />」で指定して解決。コンパイル時のみの情報。実行時の依存関係は各自、HTMLで解決したり、動的スクリプトロード用のライブラリかなんかで解決すべし。
  2. importはCommonJS/AMDのmodule仕様で「動的に外部スクリプトファイルをロードする」為のもの。動的ロードがどうしても必要じゃないならimportは無理に使わなくて良い。単にHTMLでscriptタグで読み込めばOK。なんだったら tsc hoge.ts --out all.js でまとめて1ファイルのJSにしても良い。
  3. exportキーワードはmodule内で使えばスコープ制御の意味に、module外で使えばCommonJS/AMD仕様の為のものになるのでちょっと紛らわしい。基本はmodule内でだけ使えばOKか。
  4. module定義(名前空間)したファイルをimportしようとすると名前空間が一段階ネストしちゃって気持ち悪い。これは私の使い方が悪いからかも。module定義するなら基本はimport使わずに依存関係を解決しよう。
  5. 追記:module外のオブジェクトに「export」を付けて、他のファイルから「import」で動的にロードできるようにしたものを「External Modules」(外部モジュール)と呼び、通常のmoduleキーワードで定義する名前空間のようなものを「Internal Modules」(内部モジュール)と呼んで区別している。これらはまったく別物。基本は内部モジュールだけ考えてればOKか。

とりあえずはimportについては自分の場合、使う必要がなさそうなので、いったん頭から消して、ブラウザで使う為の快適環境構築を目指してみたいと思います。

【追記】並行してStackOverflowにも質問投げてたのですが、そちらの回答も、「/// <reference path="" />の参照指定はあくまでもコンパイル時のもので、実行時の依存関係の解決は君自身の責務だよ」というものでした。実際の解決策も、exportとimportを使ったもので、あーやっぱこうなるのかという感じでした。とりあえず言語仕様にキーワードの混乱があるように思うので、このあたり近いうちに是正されるといいな、と…。

【追記】TypeScriptで画像ビュアーを作る過程を記事にしてみました。→TypeScript+jQueryでオブジェクト指向で画像ビュアー作った
Comments