#目的
TypeScriptをもっと深く知ろうと思ってます。文法とかの細かい部分をやる前に、モジュールレベルでの理解を深めようと思いました。特にモジュールの参照や参照解決辺りで発生するエラーは、基本をしっかり理解できてないとトラブルシューティングするときかなりストレス感じます。なので、まずはモジュール解決の基礎の部分をしっかり理解しようとうのが動機です。
#モジュール解決方式
murankさんがこちらで日本語訳して解説しているNode方式を使います。Classic方式というのもありますが、これは「後方互換のため」とあるので使わないことにしました。解決のアルゴリズムも文字通りNodeのやり方なので、覚えて損はないでしょう。
モジュール解決方式はtsconfig.json
で以下のように、Node方式でやっておくれ、っと指定してます。
{
...
"moduleResolution": "node"
...
}
すべてのデモはこの設定になってます。
#デモのソースコード
ソースは全てGitHubのレポジトリに置いてあります。
モジュール解決のパターンそれぞれにブランチを作ってますので、任意のブランチに変更しながら遊んで見てください。
typescript/module-demo-01
typescript/module-demo-02
typescript/module-demo-03
typescript/module-demo-04
typescript/module-demo-05
プロジェクトはdemo/typescript/module-demo というサブフォルダにあります(デモは全てそこにcdしてから実行してください)
それぞれのパターンを試すには、下のようにgit checkout
でブランチをダウンロードして、npm install
してやってください。
git checkout をするたびに、一応念のため
git clean -fd
を実行することをお勧めします。別のブランチから置き去りになったファイルなどがモジュール解決に関与して、デモがうまく行かない場合がありますから。
##解決パターン1
murankさんの、この記事の相対パスの場合の1.に該当するパターンです。
デモ: https://github.com/yoshiwatanabe/demo/tree/typescript/module-demo-01
アプリケーション内でのモジュール参照の解決としては最も一般的で理解しやすい「moduleAが何かをmoduleBからインポートする」というパターンです。このデモでは、moduleB内のEntryというクラスをインポートして、それのインスタンスを作ってnameメソッドを呼び出してます。
import { Entry } from './moduleB';
console.log((new Entry()).name());
export class Entry {
public name(): string {
return 'my entry';
}
}
実際にビルド(トランスパイル)してみますが、ここではtsc
に--traceResolution
というオプションを渡して、実際にどのようにモジュール解決が行われたかを表示させます。
「./moduleBを解決してみま~す」
「あ、候補を見つけました~」
「うまいこと解決できました~!」
というくだりのメッセージが見えますね。
あと、最後にnode src\moduleA
で一応実行もしてみてます。
#解決パターン2
murankさんの、この記事の相対パスの場合の4.に該当するパターンです。
デモ: https://github.com/yoshiwatanabe/demo/tree/typescript/module-demo-02
これはmoduleBという名前のフォルダー(ファイルではないです!)にモジュールの内容がある場合です。
moduleAの内容は解決パターン1と全く同じです。
import { Entry } from './moduleB';
console.log((new Entry()).name());
moduleBフォルダーにはファイルが二つ、置いてあります。
mainModule.ts がmoduleB.tsと同じ内容になってます。
export class Entry {
public name(): string {
return 'my entry';
}
}
そして、package.json は下のようになってます。typingsというプロパティーが重要です。
{
"name": "moduleb",
"author": "author name",
"version": "1.0.0",
"main": "mainModule.js",
"typings": "mainModule.ts"
}
この、typingsプロパティーの値がmainModule.tsになってます。
ビルドと実行をしてみるとこうなります。
「./moduleBの解決をしてみま~す」
「moduleB.tsというファイルを探してみましたが、ダメでした・・・」
「moduleB.tsxというファイルを探してみましたが、ダメでした・・・」
「moduleB.d.tsというファイルを探してみましたが、ダメでした・・・」
「お!package.jsonをmoduleBというフォルダ内に発見しました!」
「typingsフィールドを発見!mainModule.tsというファイルを発見!」
「これで良さそうなんで、これ使いまっす!」
「うまいこと解決できました~!」
という流れになってます。
#解決パターン3
murankさんの、この記事の相対パスの場合の5.に該当するパターンです。
デモ: https://github.com/yoshiwatanabe/demo/tree/typescript/module-demo-03
これは解決パターン2に良く似てて、やはりmoduleBというフォルダにモジュールがあります。ただ、異なる点はmoduleBフォルダにindex.tsという名前のファイルがひとつだけある、という点です。
index.ts の中身は最初のmoduleB.tsと同じです。
export class Entry {
public name(): string {
return 'my entry';
}
}
トランスパイルと実行してみます。
パターン2とよく似てますが、.ts、.tsx、.d.tsの拡張子のファイルを探すのを諦めたあと、package.jsonも諦めて、最後にindex.tsをmoduleBフォルダ内で発見しています。それでめでたくモジュール解決が成功しました。
#解決パターン4
murankさんの、この記事の非相対パスの場合の5.に似ているパターンです。
デモ: https://github.com/yoshiwatanabe/demo/tree/typescript/module-demo-04
「該当する」ではなく「似ている」としたのは、デモで使っているモジュールがTypeScriptではなく普通のJavaScriptのモジュールなので、@typesという仕組みを使ってモジュールをインポートしているからです
(なぜちゃんとTypeScriptのデモを作らなかったかの言い訳)
サンプルになる TypeScriptで書かれた、依存の少ないモジュールを探したんですが、見つけられませんでした。サンプルになるTypeScriptモジュールを自分で作ってnpmレジストリにアップロードしようかとも思いましたがnpmレジストリのアカウント取得でメアドを公開しなくちゃいけないってことで、諦めました。Visual Studio Team Services(VSTS)が提供するnpmレジストリを使おうかとも思ったんですが、認証とか面倒なんで止めました。コンセプトとしては@typesの仕組みを使ったものもネイティブにTypeScriptのものも同じです。
下はpackage.jsonで、color-nameというモジュールをdependenciesに指定している様子です。color-nameモジュールは単体で色の名前とRGBの値をマップしている定数、という単純なものです。
{
"name": "demo",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"tsc": "tsc",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@types/color-name": "^1.1.0",
"color-name": "^1.1.3",
"typescript": "^2.4.2"
}
}
トップレベルのnode_modulesフォルダ下に、@typesフォルダーがあります。そのすぐ下にcolor-nameというフォルダがあり、さらにその下にindex.d.tsというファイルがあるのが分かります。
メインのモジュールであるmoduleA.tsは下のように変更して、color-nameモジュールからアクアマリン色のRGB値を得るようにしています。
import * as colorName from "color-name";
console.log('RGB value of Aquamarine is ' + colorName.aquamarine);
ところで"color-name"(非相対パス)です。"./color-name"(相対パス)でないことに注意!
このパターンはnpmレジストリからダウンロードしてきたモジュールを利用する場合に一般的なやり方です。(今回はJavaScriptモジュールを利用する場合、ですが)
以下はトランスパイルと実行の様子。
赤で示された部分でモジュールが解決したのが分かります。例によって、.ts拡張子のファイルやpackage.jsonファイル、それにindexファイルを含むcolor-nameフォルダなどを探している様子が分かります。
ここで注目すべき点はnode_modules内で探していることです。先に述べたようにnpmレジストリからinstallしたモジュールを利用する場合に起こるモジュール解決だということが分かります。
青で示された部分でType reference directiveが解決された、とあります。これは赤の部分のModuleが解決された、とは異なります。この部分はまだ勉強していませんから、よく分かりません。おそらく@typesの仕組みを使った場合に必要なステップかと思われます。
#解決パターン5
最後のデモです。
murankさんの、この記事の非相対パスの場合の、ステップ8やステップ15のように、どんどんと上のnode_modulesフォルダを調べて行く、というパターンをデモしたものです。
デモ: https://github.com/yoshiwatanabe/demo/tree/typescript/module-demo-05
今回はsrc フォルダ内にもpackage.jsonを置いています。トップレベルにあるpackage.jsonと合わせて、二つあることになります。
やはり、npm installを実行します。
下は二つの異なるレベルでnode_modulesフォルダが存在している状態を示しています。
moduleA.tsの内容も少し変更されていて、moduleBとcolor-nameの両方をインポートしています。
import { Entry } from './moduleB';
import * as colorName from 'color-name'
console.log((new Entry()).name() + ' and RGB of Auzre is ' + colorName.azure);
(moduleBはパターン2と同じなので、ここでは無い方が分かり易かったかもしれません。moduleBは無視してください)
トランスパイルと実行をしてみます。
ここで重要なことは、解決されたcolor-nameモジュールが、トップレベルのnode_packagesではなくてsrc下にある、src\node_modulesであるということです。モジュール解決はこのように直近のnode_modulesフォルダから(もし存在するのなら)スキャンが始まって、順次上の階層に移動してスキャンが続けられます。
複数のnode_modulesがある状態がベスト・プラクティスという点で良いのか、というと疑問ですが、こういうパターンでのモジュール解決もありうる、ということを知っておくのはトラブルシューティングをする上で有益かもしれません。
#まとめ
モジュール解決の基本的なパターンをデモしました。私自身まだ学習中で、モジュール関連の設定はまだたくさんあります。この辺りの理解が少しでも進むと、より楽しくTypeScriptを使えると思います。