モジュール解決

このセクションは、基本的なモジュールの知識があることを前提にしています。 詳しい情報については、モジュールのドキュメントを参照してください。

モジュール解決は、importが何を参照するのかを把握するために、コンパイラを利用するプロセスになります。

import { a } from "moduleA";のようなimport文について考えてみてください。 aの使用するために、コンパイラは正確に何が提供されているのかを知る必要があり、 またmoduleAの定義を調べる必要もあります。

ここで、コンパイラは"moduleAはどのような形状をしていますか?"と尋ねます。 これは一見単純なように見えますが、コードによってmoduleA.ts/.tsxファイル、 または.d.tsファイルのいずれかに定義されることになります。

まず、コンパイラはimportされているモジュールのファイルを見つけようとします。 これを行うには、コンパイラは2つの異なる方針、クラシックまたはNodeのどちらかに従います。 これらの方針が、moduleAを探す場所をコンパイラに教えます。

これが動かなかった場合、モジュール名に相対関係が無かった場合(ここでは"moduleA")、 コンパイラはambientモジュール宣言を見つけようとします。 この次に、非相対のimportについて説明します。

最終的にコンパイラがモジュールの解決が出来なかった場合、エラーログを出力します。 このケースでは、error TS2307: Cannot find module 'moduleA'のようなエラーが出力されます。

相対(Relative) vs 非相対(Non-relative)モジュールimport

モジュールのimportは、モジュール参照が相対か非相対のどちらかをもとに異なる解決がされます。

相対importは、/./、または../のいずれかから始まります。 下記に幾つかの例を示します。

  • import Entry from "./components/Entry";
  • import { DefaultHeaders } from "../constants/http";
  • import "/mod";

これ以外のimportは非相対とみなされます。下記に幾つかの例を示します。

  • import * as $ from "jquery";
  • import { Component } from "angular2/core";

相対importは、importするファイルを相対関係によって解決し、ambientモジュール宣言を相対関係で解決することはできません。 自身が開発した実行時に相対関係の保持が保証されるモジュールに対しては、 相対importを使用するべきです。

非相対importは、baseUrlの相対、またはパスのマッピングを通して解決され、 これについては後ほど説明します。 これらは、ambientモジュール宣言の解決も行います。 外部に依存関係を持つものをインポートする際は、非相対パスを使用してください。

モジュール解決の方針

モジュール解決の方針には、Nodeクラシックの2つが存在します。 --moduleResolutionを使用して、モジュール解決の方針を指定することが可能です。 指定されない場合、デフォルトはNodeになります。

クラシック

これは、TypeScriptのデフォルトの解決の方針として使用されます。 最近では、主にこの方針が後方互換のために提供されています。

相対importはインポートするファイルを相対的に解決します。 そのため、ソースファイルである/root/src/folder/A.ts内のimport { b } from "./moduleB"は、 次のようにファイルを探します。

  1. /root/src/folder/moduleB.ts
  2. /root/src/folder/moduleB.d.ts

非相対モジュールのインポートに対しては、コンパイラはimportするファイルを含むディレクトリを出発点としてディレクトリツリーを巡り、 目的の定義ファイルを見つけ出そうとします。

例えば、

ソースファイル/root/src/folder/A.ts内でimport { b } from "moduleB"のように、 非相対的にmoduleBをインポートする場合は、"moduleB"を探すために次のような検索が行われます。

  1. /root/src/folder/moduleB.ts
  2. /root/src/folder/moduleB.d.ts
  3. /root/src/moduleB.ts
  4. /root/src/moduleB.d.ts
  5. /root/moduleB.ts
  6. /root/moduleB.d.ts
  7. /moduleB.ts
  8. /moduleB.d.ts

Node

この解決方針は、実行時のNode.jsのモジュール解決のメカニズムを真似ることを試みたものです。 完全なNode.jsの解決アルゴリズムは、 Node.jsのモジュールのドキュメントに概要が説明されています。

Node.jsのモジュール解決の方法

TSコンパイラがどのような手順を踏んでいるのかを理解するには、 Node.jsモジュールにスポットライトを当てることが重要です。 伝統的に、Node.js内でのimoportはrequireという名前の関数を呼び出すことで実行されます。 Node.jsでの動きは、requireが相対パスまたは非相対パスのどちらが与えられているかで、挙動が異なります。

相対パスは、かなり分かりやすいものです。 例として、import var x = require("./moduleB");を含む/root/src/moduleA.jsにファイルが配置されていたとして、 Node.jsは次の順番でimportの解決を図ります。

  1. 存在すれば、ファイル名/root/src/moduleB.jsとして解決します。
  2. package.jsonという名前の"main"モジュールを指定するファイルが含まれていれば、 /root/src/moduleBフォルダとして解決します。

    この例であれば、Node.jsは{ "main": "lib/mainModule.js" }を含む/root/src/moduleB/package.jsonを見つけ、 /root/src/moduleB/lib/mainModule.jsを参照します。

  3. index.jsという名前のファイルを含んでいれば、/root/src/moduleBのフォルダとして解決します。 このファイルは暗黙的に、フォルダの"main"モジュールとみなされます。

この詳細については、 Node.jsのドキュメントのファイルモジュールフォルダモジュールを参照してください。

ただし、非相対モジュールの名前解決は異なる動きをします。 Nodeはnode_modulesという名前の特別な名前のフォルダ内から、モジュールを探します。 node_modulesフォルダは現在のファイルの同階層、またはディレクトリのチェーン上の上層に配置することが可能です。 Nodeはディレクトリチェーンを巡り、読み込もうとしているモジュールを見つけるまで、各node_modulesを探します。

前述の例を、仮に/root/src/moduleA.jsが非相対パスを使用するとして、 var x = require("moduleB");のimport指定があったとしましょう。 NodeはmoduleBの解決を、それが成功するまで各場所で試みます。

  1. /root/src/node_modules/moduleB.js
  2. /root/src/node_modules/moduleB/package.json("main"指定があれば)
  3. /root/src/node_modules/moduleB/index.js

  4. /root/node_modules/moduleB.js
  5. /root/node_modules/moduleB/package.json("main"指定があれば)
  6. /root/node_modules/moduleB/index.js

  7. /node_modules/moduleB.js
  8. /node_modules/moduleB/package.json("main"指定があれば)
  9. /node_modules/moduleB/index.js

Node.jsがディレクトリを(4)と(7)で遡っていることに注目してください。

このプロセスの詳細については、Node.jsのドキュメントのnode_modulesからのモジュール読み込みで読むことができます。

TypeScriptのモジュール解決方法

TypeScriptはコンパイル時にモジュールのための定義ファイルを探すために、Node.jsのランタイム時の解決方針を真似します。 これを実現するために、TypeScriptはNodeの解決ロジックに対して、TypeScriptのソースファイル拡張子(.ts、.tsx、.d.ts)を覆い被せます。

また、TypeScriptはpackage.json内の"typings"と名付けられたフィールドを、 "main"の意図を映すのにも使用します。 コンパイラはこれを"main"定義ファイルを見つけるために使用します。

例えば、/root/src/moduleA.ts内のimport { b } from "./moduleB"のようなimport文は、 結果的に"./moduleB"を、次のように探すことを試みます。

  1. /root/src/moduleB.ts
  2. /root/src/moduleB.tsx
  3. /root/src/moduleB.d.ts
  4. /root/src/moduleB/package.json("typings"があれば)
  5. /root/src/moduleB/index.ts
  6. /root/src/moduleB/index.tsx
  7. /root/src/moduleB/index.d.ts

Node.jsのことを振り返ってみると、moduleB.jsという名前のファイルを探し、 次に該当するpackage.jsonを、その次にindex.jsを探していました。

同様に非相対importはNode.jsの解決ロジックに従い、まずファイルを探し、次に該当するフォルダを探します。 そのため、ソースファイル/root/src/moduleA.ts内のimport { b } from "moduleB"は、 結果的に次の順番で検索します。

  1. /root/src/node_modules/moduleB.ts
  2. /root/src/node_modules/moduleB.tsx
  3. /root/src/node_modules/moduleB.d.ts
  4. /root/src/node_modules/moduleB/package.json("typings"があれば)
  5. /root/src/node_modules/moduleB/index.ts
  6. /root/src/node_modules/moduleB/index.tsx
  7. /root/src/node_modules/moduleB/index.d.ts

  8. /root/node_modules/moduleB.ts
  9. /root/node_modules/moduleB.tsx
  10. /root/node_modules/moduleB.d.ts
  11. /root/node_modules/moduleB/package.json("typings"があれば)
  12. /root/node_modules/moduleB/index.ts
  13. /root/node_modules/moduleB/index.tsx
  14. /root/node_modules/moduleB/index.d.ts

  15. /node_modules/moduleB.ts
  16. /node_modules/moduleB.tsx
  17. /node_modules/moduleB.d.ts
  18. /node_modules/moduleB/package.json("typings"があれば)
  19. /node_modules/moduleB/index.ts
  20. /node_modules/moduleB/index.tsx
  21. /node_modules/moduleB/index.d.ts

このステップ数に怯えることはありません。 TypeScriptは相変わらず(8)と(15)で2度ディレクトリを遡っているにすぎません。 これはNode.js自身が行っている以上の複雑さはありません。

拡張モジュール解決フラグ

プロジェクトのソースファイルの配置は、時に出力の配置と一致しないことがあります。 通常であればビルド結果は、最終的な出力を生成する結果となります。

これらは、.tsファイルを.jsファイルにコンパイルし、 依存性を異なるソースの複数の配置場所から単一の出力場所にコピーすることを含みます。 その結果、ランタイム時のモジュールは、それらの定義を含むソースファイルのものとは異なる名前になる可能性があります。 また、最終的な出力のモジュールのパスは、コンパイル時のソース・ファイルのパスに対応したものと一致しない可能性があります。

TypeScriptのコンパイラには、最終的な出力の生成で期待されるされることを、 変換時にコンパイラに伝えるための追加のフラグが用意されています。

コンパイラはこれらの変換のいずれかを実行しないことに注意しなければいけません。 これらの情報の一部分を、定義ファイルのimportによるモジュール解決の指針として使用しているに過ぎません。

Base URL

baseUrlの使用は、モジュールがランタイム時に単一のフォルダに"配置(deployed)"されている状況で、 AMDモジュールローダーを使用するアプリケーションの慣習となっています。 これらモジュールのソースファイルは異なるディレクトリに配置することが可能ですが、 ビルドスクリプトで全てまとめられます。

baseUrlを設定することで、コンパイラにモジュールを探す場所を伝えます。 非相対の全てのモジュールのimportは、baseUrlへの相対とみなされます。

baseUrlの値は下記のいずれかによって決定されます。

  • コマンドライン引数baseUrlの値 (指定されたパスが相対の場合、現在のディレクトリが基準となります)
  • 'tsconfig.json'内のbaseUrlプロパティの値 (指定されたパスが相対の場合、'tsconfig.json'が配置されている場所が基準となります)

相対モジュールのimportは常にimportするファイルが相対的に解決されるため、 baseUrlの設定による影響を受けないことに注意してください。

詳細については、 RequireJSと、 SystemJSのドキュメントを参照してください。

パス・マッピング

場合によって、モジュールがbaseUrl下に直接配置されないことがあります。 例えば、"jquery"モジュールのimportは、実行時には"node_modules\jquery\dist\jquery.slim.min.js"に変換されます。 ローダーは実行時にモジュール名をファイルに紐付けるマッピング設定を使用します。 詳細については、RequireJsのドキュメントと、 SystemJSのドキュメントを参照してください。

TypeScriptのコンパイラは、このようなマッピングの宣言をtsconfig.jsonファイル内の"paths"プロパティによってサポートします。 下記は、jQueryのために"paths"プロパティを指定する例になります。

{
  "compilerOptions": {
    "paths": {
      "jquery": ["node_modules/jquery/dist/jquery.d.ts"]
    }
}

"paths"を使用すると、複数のフォールバック位置を含む、より洗練されたマッピングも可能になります。 1つの場所でいくつかのモジュールしか利用できず、残りのモジュールが別の場所にあるプロジェクト構成があるとすると、 ビルドステップで、全てが1つの場所にまとめられます。 プロジェクトのレイアウトが、下記の構成だとすると、

projectRoot
├── folder1
│   ├── file1.ts (imports 'folder1/file2' and 'folder2/file3')
│   └── file2.ts
├── generated
│   ├── folder1
│   └── folder2
│       └── file3.ts
└── tsconfig.json

対応するtsconfig.jsonはこのようになります。

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "*": [
        "*",
        "generated/*"
      ]
    }
  }
}

これは、パターン "*"(すなわち、すべての値)と一致するモジュールのインポートをコンパイラに指示し、2つの場所を検索します。

  1. "*": 同じ名前で変更されないことを意味し、次のようにマップします。 <moduleName> => <baseUrl>\<moduleName>
  2. "generated\*"は接頭辞として"generated"をモジュール名の前に付けるため、次のようにマップします。 <moduleName> => <baseUrl>\generated\<moduleName>

このロジックに従い、コンパイラは次のようにして2つのインポートの解決を試みます。

  • import 'folder1/file2'
    1. パターン'*'がマッチし、ワイルドカードは全モジュール名を取得します。
    2. 1つ目の置換を試みます: '*' -> folder1/file2
    3. 置換の結果をbaseUrlを連結して相対的な名前にします: -> projectRoot/folder1/file2.ts
    4. このファイルが存在するので、完了となります。
  • import 'folder2/file3'
    1. パターン'*'がマッチし、ワイルドカードは全モジュール名を取得します。
    2. 1つ目の置換を試みます: '*' -> folder2/file3
    3. 置換の結果をbaseUrlを連結して相対的な名前にします: -> projectRoot/folder2/file3.ts
    4. ファイルが存在しないため、2つ目の置換処理に移行します。
    5. 2つ目の置換: 'generated/*' -> generated/folder2/file3
    6. 置換の結果をbaseUrlを連結して相対的な名前にします: -> projectRoot/generated/folder2/file3.ts
    7. このファイルが存在するので、完了となります。

rootDirを使用した仮想ディレクトリ

時折、コンパイル時に複数のディレクトリから構成されるプロジェクトのソースが、 1つの出力ディレクトリに生成されるために、全てが結合されることがあります。 この場合、ソースのディレクトリの集まりとして"仮想"ディレクトリとしてみなすことができます。

'rootDirs'を使用することで、この"仮想"ディレクトリを構成するrootをコンパイラに伝えることができます。 コンパイラは、これらの「仮想」ディレクトリ内の相対モジュールのインポートを、 1つのディレクトリにまとめてマージしたかのように解決することができます。

例えば、次のようなプロジェクト構成があるとします。

 src
 └── views
     └── view1.ts (imports './template1')
     └── view2.ts

 generated
 └── templates
         └── views
             └── template1.ts (imports './view2')

src/viewsのファイルは幾つかのUI制御のためのユーザーコードです。 generated/templatesのファイルは、 ビルドの一部としてテンプレートのジェネレーターによって自動生成されるコードに紐づくUIテンプレートです。 ビルドの段階で、/src/views/generated/templates/viewsのファイルがコピーされ、同じディレクトリに出力されます。 実行時には、viewはそのテンプレートが同ディレクトリ内に存在すると期待するため、 相対名"./template"を使用してimportしようとするでしょう。

この関係性をコンパイラに伝えるために、"rootDirs"を使用します。 "rootDirs"には、実行時にマージされることが期待されるコンテンツのルートのリストを指定します。 先程の例であれば、tsconfig.jsonファイルは次のようになります。

{
  "compilerOptions": {
    "rootDirs": [
      "src/views",
      "generated/templates/views"
    ]
  }
}

コンパイラはrootDirsの各サブフォルダ内のモジュールの相対インポートを見つける度に、 rootDirsの各エントリでこのインポートを探そうとします。

モジュール解決のトレース

前述したように、コンパイラはモジュールを解決する際に、現在のフォルダの外にあるファイルを参照することができます。 これはモジュールが解決されなかった理由を診断するときや、誤った定義で解決されようとしたときには厳しい場合があります。 --traceResolutionを使用してコンパイラによるモジュール解決のトレースを有効にすると、 モジュール解決プロセス中に何が起こったかがわかります。

typescriptモジュールを使用するサンプルアプリケーションがあり、 app.tsにはimport * as ts from "typescript"のインポートがあるとします。

│   tsconfig.json
├───node_modules
│   └───typescript
│       └───lib
│               typescript.d.ts
└───src
        app.ts

--traceResolutionを使用してコンパイラを実行します。

tsc --traceResolution

出力される結果は次のようになります。

======== Resolving module 'typescript' from 'src/app.ts'. ========
Module resolution kind is not specified, using 'NodeJs'.
Loading module 'typescript' from 'node_modules' folder.
File 'src/node_modules/typescript.ts' does not exist.
File 'src/node_modules/typescript.tsx' does not exist.
File 'src/node_modules/typescript.d.ts' does not exist.
File 'src/node_modules/typescript/package.json' does not exist.
File 'node_modules/typescript.ts' does not exist.
File 'node_modules/typescript.tsx' does not exist.
File 'node_modules/typescript.d.ts' does not exist.
Found 'package.json' at 'node_modules/typescript/package.json'.
'package.json' has 'typings' field './lib/typescript.d.ts' that references 'node_modules/typescript/lib/typescript.d.ts'.
File 'node_modules/typescript/lib/typescript.d.ts' exist - use it as a module resolution result.
======== Module name 'typescript' was successfully resolved to 'node_modules/typescript/lib/typescript.d.ts'. ========

各行の説明

  • 名前とインポート場所
    ======== Resolving module 'typescript' from 'src/app.ts'. ========
    
  • コンパイラが従う戦略
    Module resolution kind is not specified, using 'NodeJs'.
    
  • npmパッケージからのtypingsの読み込み
    'package.json' has 'typings' field './lib/typescript.d.ts' that references 'node_modules/typescript/lib/typescript.d.ts'.
    
  • 最終的な結果
    ======== Module name 'typescript' was successfully resolved to 'node_modules/typescript/lib/typescript.d.ts'. ========
    

--noResolveの使用

通常コンパイラは、コンパイル処理が開始される前に、全てのモジュールインポートを解決しようと試みます。 インポートするファイルの解決に成功すると、そのファイルはコンパイラが後で処理をするファイルの集まりに加えられます。

--noResolveコンパイラオプションは、コンパイラで渡されなかったファイルを、 コンパイルに"追加"しないようにコンパイラに指示します。 それでもモジュールのファイルを解決しようとしますが、指定されていないファイルが含まれることはありません。

例えば、

app.ts

import * as A from "moduleA" // OK, 'moduleA' passed on the command-line
import * as B from "moduleB" // Error TS2307: Cannot find module 'moduleB'.
tsc app.ts moduleA.ts --noResolve

--noResolveを使用したapp.tsのコンパイルの結果は次のようになるはずです。

  • コマンドラインで渡されたとして、moduleAは正しく検索されます。
  • コマンドラインで渡されなかったので、moduleBの検索はエラーになります。

よくある質問

何故、除外(exclude)リストのモジュールがコンパイラによって引き上げられるのですか?

tsconfig.jsonはフォルダを"プロジェクト"に変換します。 "exclude"または"files"が指定されていないものは、 tsconfig.jsonを含む全てのフォルダとそのサブフォルダ内のファイルがコンパイルに含まれることになります。

もし、"exclude"を使用して特定のファイルを除外したい場合でも、 コンパイラに全てのファイルを参照させる代わりに、"files"を使用して全てのファイルを指定するのがよいかもしれません。(翻訳に自信なし)

 Back to top

© https://github.com/Microsoft/TypeScript-Handbook

このページは、ページトップのリンク先のTypeScript-Handbook内のページを翻訳した内容を基に構成されています。 下記の項目を確認し、必要に応じて公式のドキュメントをご確認ください。 もし、誤訳などの間違いを見つけましたら、 @tomofまで教えていただければ幸いです。

  • ドキュメントの情報が古い可能性があります。
  • "訳注:"などの断わりを入れた上で、日本人向けの情報やより分かり易くするための追記を行っている事があります。