TypeScript のモジュールの使い方

この記事は ASP.NET Advent Calendar 2015 の 11 日目の記事です。

最近 JavaScript 系の技術に興味を持って、Angular を中心に調べています。
やっぱり MS クラスターとしては、生の JavaScript よりも TypeScript を書きたいじゃないですか。
今回は JavaScriptNode.js)と TypeScript のモジュール機構についてまとめてみました。

というわけでこれを読め

qiita.com

わかめさんが既に素晴らしい記事を書いてくださっています。
本記事はこの記事にちょっと蛇の足を生やした程度のものです。

Node.js のモジュール機構(JavaScript の場合)

TypeScript の前に、まずは Node.js のモジュール機構について。
ASP.NET のフロントエンドで JavaScript を使う場合には、Node.js のお世話になる機会は少ないと思いますが、最近は ASP.NET でも gulp 等のツールを使うのが一般的になって来ていますので、知っていて損はありません。

ディレクトリ構造

Node.js のパッケージマネージャーである npm を使う場合、プロジェクト ディレクトリは以下のような構成になります(gulp をインストールした場合)。

  • プロジェクト ディレクトリ
    • node_modules
      • gulp
        • package.json
        • index.js
        • ...
      • ...
    • package.json
    • gulpfile.js
    • sub.js

プロジェクト ディレクトリ直下にある package.json は、このプロジェクトで使用している npm モジュールへの参照が書かれています(npm install コマンドで書き込まれます)。
node_modules ディレクトリの下に、npm でインストールした各モジュールのディレクトリが作られます。

gulp ディレクトリの下にある package.json には、gulp モジュールが依存しているモジュールの一覧をはじめ、gulp モジュールの構成情報が書かれています。

sub.js は gulpfile.js の中で使われるファイルだと思ってください。

モジュールのインポート

gulpfile.js 中では、こんな風にして他のモジュールを読み込みます。

var gulp = require("gulp");
var sub = require("./sub");

Node.js では require 関数でモジュールを読み込みます。

1 行目は gulp モジュールを読み込んでいます。
パスを指定せずモジュール名だけを書くと、node_modules ディレクトリ下にあるモジュールを読み込みます。
この場合 Node.js は、node_modules/gulp ディレクトリの中の package.json を見に行きます。
package.json に main エントリーがあれば、それがこのモジュールのエントリー ポイントになりますので、そのファイルを実行します。
gulp の場合は main エントリーが無いので、この場合、index.js が実行されます。

2 行目はプロジェクト ディレクトリ下にある sub.js を読み込んでいます。
npm のモジュールではなく、他の JavaScript ファイルを読み込む場合、同じディレクトリにあっても、パス(./)を付ける必要があります。
拡張子 .js は付けても付けなくても構いませんが、一般的には付けません。

モジュールからのエクスポート

Node.js でモジュールから機能をエクスポートするための書き方はいくつかあります。
例えば、sub.js から関数 Hoge をエクスポートするには、以下のように書きます。

/* [1] */ exports.Hoge = function () { ... };
/* [2] */ module.exports.Hoge = function () { ... };
/* [3] */ module.exports = function () { ... };

[1] は CommonJS という仕様に従った書き方です。
[2] と [3] は Node.js 独自の書き方です。
gulp のプラグインを作る場合は [3] の書き方をするのが一般的なようですが、この方法は後述する ES6 形式のインポートと互換性がないので、個人的には推奨しません。

それぞれの方法でエクスポートした関数を、インポートした側で使う方法は以下のようになります。

var sub = require("./sub");

// [1] [2] の場合
sub.Hoge();

// [3] の場合
sub();

[1] と [2] の場合、require("./sub") によって sub モジュールを読み込んでおり、変数 sub にはモジュールそのものが入っているイメージです。
sub.Hoge(); とすることで、sub モジュールからエクスポートされている Hoge 関数を呼び出しています。

TypeScript の場合

やっと本題です。

TypeScript も歴史的経緯から、モジュールに関してはいろいろな書き方があるのですが、本記事では、今後主流になるであろう最新の書き方だけを紹介します。
TypeScript は ECMAScript 2015(ES2015、ES6 とも言う)の仕様を先取りしていますので、モジュールも ES6 形式で書きます。

ディレクトリ構造

TypeScript を使うにあたって、ちょっと変更しています。

  • プロジェクト ディレクトリ
    • node_modules
      • gulp
        • package.json
        • index.js
        • ...
      • ...
    • package.json
    • tsconfig.json
    • gulpfile.ts
    • sub.ts

gulpfile.ts は gulpfile.js を TypeScript で書いたものだと思ってください。*1
sub.ts は gulpfile.ts の中から使われるファイルです。

tsconfig.json は TypeScript のプロジェクト定義ファイルです。
このファイルがあるディレクトリで

tsc

と打つと、コマンドライン オプションをいちいち指定しなくても、tsconfig.json の内容に従ってコンパイルしてくれます。

モジュールのインポート

こんな風に書きます。

import * as gulp from "gulp";
import { Hoge } from "./sub";

1 行目は gulp モジュールを gulp という名前でインポートしています。JavaScript で言うと

var gulp = require("gulp");

と同じです。

2 行目は sub.ts ファイルがエクスポートするもののうち、Hoge だけを読み込んでいます。
この場合は 1 行目のように、モジュールそのものを指す変数はありませんので、関数であれば

Hoge();

のように直接使います。

JavaScript の時と同様、モジュール名だけを書くと node_modules にあるモジュールを、パスを付けて書くと指定した .ts ファイルを読み込みます。

名前に別名を付けることもできます。

import { Hoge as foo } from "./sub";

と書くと、Hoge の代わりに foo で参照できます。

拡張子を付けないこと

ここで、ローカル ファイルを読み込む時に拡張子を書かないことが重要です。
sub.ts からは sub.js が作られますが、

// これは TypeScript のコード
import { foo } from "./sub";
import { bar } from "./sub.ts";

こう↑書くと、こう↓翻訳されます。

// これは JavaScript のコード
var sub_1 = require("./sub");
var sub_ts_1 = require("./sub.ts");

2 行目は動きませんね。

型定義ファイル

先のインポートのコードは、実は動きません。
TypeScript のソースは TypeScript のモジュールしか読み込まないからです。
gulp は TypeScript ではなく JavaScript のモジュールです。

JavaScript のモジュールでも、JavaScript の構文を使って読み込むことはできます。
つまり、こう。

// これは .ts ファイルです
var gulp = require("gulp");

ただし、静的な型チェックは一切効きません。

というわけで、既存の JavaScript モジュールに対しても型チェックをするために、型宣言だけを TypeScript で書いたファイルというのがあります。
それが型定義ファイルという、.d.ts という拡張子のファイルです。

DefinitelyTyped という(いつまで経っても綴りが覚えられない)サイトには、世の中の多くの JavaScript ライブラリに対する型定義ファイルが集められています。
gulp に対する gulp.d.ts もあります。

DefinitelyTyped から型定義ファイルを取ってくるのには、dtsm というツールが便利です。
詳しくはこちら。
qiita.com

型定義ファイルを手に入れたら、tsconfig.json に書いておきましょう。

型定義ファイルを書く

しかし、DefinitelyTyped に型定義ファイルが無かったり、あっても古くて互換性が無かったりする場合もあります。
そういう場合は、仕方がないので自分で書きます。

ここで型定義ファイルの書き方を書いているとキリがありませんので割愛させて頂きます。
ただ、何点か注意事項。

  • module というキーワードは最近では namespace に取って代わられたようなことも聞きますが、型定義ファイルを書くときの declare module という文脈では生きています。declare namespace にはできません。
  • declare module の中ではエクスポートするクラスや関数に export キーワードを付ける必要はありません(付けても構いません)。

モジュールからのエクスポート

先程の話は、TypeScript 製でない JavaScript モジュールの話でした。
ここからは TypeScript でモジュールを書く場合の話。

TypeScript のモジュールは、ファイルのトップレベルに export キーワードを伴った関数やクラスがあるファイルのことです。
エクスポートできるものは以下の通り。

default というキーワードを付けてエクスポートすることもできます。
default 付きでエクスポートできるのは、クラスと関数だけです。

default 付きでエクスポートしたものは、"default" という名前でエクスポートされます。
名前を付けなくても構いませんし、何か名前を付けても、その名前はモジュール内からのみ参照でき、外部からは利用できません。

default 付きでエクスポートした場合、インポート側でも特別な書き方をします。

// エクスポート側
export default function ()
{
  // ...
}

// インポート側
import hoge from "hoge";
import { default as hoge } from "hoge";
hoge();

default をインポートする場合は、名前付きの識別子をインポートする場合と異なり、{} を付けません。
あるいは、{ default as 別名 } と書いて別名を付けます。

基本的には、export default を利用し、1 モジュール 1 export にするのが推奨のようです。

型定義ファイルの配布

TypeScript でモジュールを書いて、コンパイルする時に tsconfig.json の declarations を true にしておくと、自動的に .d.ts ファイルが作られます。
これは declare module "hoge" という形式ではなく、ファイルのトップレベルにいきなり export が書かれた形式になっています。
このファイルは JavaScript 向けのものと異なり、DefinitelyTyped で配布されている形式と互換性がありません。

TypeScript で書いたモジュールを npm を通じて配布する場合は、この型定義ファイルを配布物に含めます。
その際、package.json の typings エントリーにその名前を書くか、もしくは、"index.d.ts" という名前にしておくと、インポートする側の TypeScript コンパイラーが自動的に読み込んでくれます。

このあたりの詳細は、冒頭に挙げたわかめさんの記事が詳しいので、そちらを見て頂いた方がよいと思います。

typings エントリーは、名前が複数形(typing"s")のくせに、1 つしかファイル名を書くことができません。
では、1 つの npm パッケージに複数のモジュールを含めたい場合はどうしたらよいのでしょうか?
個人的には、すべてのエクスポートをまとめたモジュールを作ると良いのではないかと思います。

// export.ts
// foo.ts、bat.ts、baz.ts が各機能をエクスポートするモジュールだとする
export { foo } from "./foo";
export { bar } from "./bar";
export { baz } from "./baz";

このように書いて、export.d.ts を配布するというのはどうでしょうか。

いいわけ

最初は ASP.NET のことを書く予定だったんですが、そのネタが諸般の事情によりボツになりまして、急遽 TypeScript ネタに差し替えました。
まぁ、いいですよね? Microsoft のテクノロジーだし、ASP.NET でもサポートされているし…
や、JavaScript 系のアドベントカレンダーで書けってのはもっともなんですが、ほら、マサカリ飛んで来たら怖いじゃないですか。
その点、ここなら大丈夫かなぁって…

qiita.com

*1:gulp では gulpfile.ts を直接実行することもできるらしいのですが、うまく動かなかったので、一旦 gulpfile.js にコンパイルしてから動かすことを想定しています。