メインコンテンツへ移動

名前空間_TypeScript ノート 15

無料2019-04-13#TypeScript#TypeScript namespace vs module#TypeScript module keyword#TypeScript /// <reference#TypeScript internal module vs external module#TypeScript内部模块与外部模块

名前空間は JavaScript 中のモジュールパターンに由来し、もう一つのコード組織方式

一.由来

名前空間は JavaScript 中の [モジュールパターン](/articles/モジュールパターン-javascript デザインパターン 2/) に由来します:

var MyModule = {};
(function(exports) {
  // プライベート変数
  var s = "hello";
  // 公開関数
  function f() {
    return s;
  }
  exports.f = f;
})(MyModule);

MyModule.f();
// エラー MyModule.s is not a function
MyModule.s();

2 部分で構成されます:

  • モジュールクロージャ(module closure):モジュール実装をカプセル化し、スコープを隔離
  • モジュールオブジェクト(module object):該モジュールが暴露する変数と関数

後にこの基礎上でモジュール動的ロード、複数ファイルに拆分などのサポートを拡張

TypeScript はモジュールパターンとクラスパターンを結合して一種のモジュールメカニズムを実装しました。つまり名前空間:

namespace MyModule {
  var s = "hello";
  export function f() {
    return s;
  }
}

MyModule.f();
// エラー Property 's' does not exist on type 'typeof MyModule'.
MyModule.s;

コンパイル産物は古典的なモジュールパターン:

var MyModule;
(function (MyModule) {
  var s = "hello";
  function f() {
    return s;
  }
  MyModule.f = f;
})(MyModule || (MyModule = {}));
MyModule.f();
MyModule.s;

二.作用

モジュールに類似し、名前空間も一種のコード組織方式:

namespace Page {
  export interface IPage {
    render(data: object): string;
  }
  export class IndexPage implements IPage {
    render(data: object): string {
      return '<div>Index page content is here.</div>';
    }
  }
}

// コンパイル結果
var Page;
(function(Page) {
  var IndexPage = /** @class */ (function() {
    function IndexPage() {}
    IndexPage.prototype.render = function(data) {
      return '<div>Index page content is here.</div>';
    };
    return IndexPage;
  })();
  Page.IndexPage = IndexPage;
})(Page || (Page = {}));

同様にスコープ隔離(上例は Page 1 つのグローバル変数のみ暴露)を持ち、ファイル別にモジュールを拆分することもサポート:

// IPage.ts
namespace Page {
  export interface IPage {
    render(data: object): string;
  }
}

// IndexPage.ts
/// <reference path="./IPage.ts" />
namespace Page {
  export class IndexPage implements IPage {
    render(data: object): string {
      return '<div>Index page content is here.</div>';
    }
  }
}

// App.ts
/// <reference path="./IndexPage.ts" />
/// <reference path="./DetailPage.ts" />
let indexPageContent = new Page.IndexPage().render({value: 'index'})

コンパイル結果:

// IPage.js
空

// IndexPage.js
/// <reference path="./IPage.ts" />
var Page;
(function (Page) {
    var IndexPage = /** @class */ (function () {
        function IndexPage() {
        }
        IndexPage.prototype.render = function (data) {
            return '<div>Index page content is here.</div>';
        };
        return IndexPage;
    }());
    Page.IndexPage = IndexPage;
})(Page || (Page = {}));


// App.js
/// <reference path="./IndexPage.ts" />
/// <reference path="./DetailPage.ts" />
var indexPageContent = new Page.IndexPage().render({ value: 'index' });

ここで三斜線指令を通じて拆分された「namespace モジュール」を導入(module のように import ではない)ことに注意。still import を使用すると、エラーが報告されます:

// エラー File '/path/to/IndexPage.ts' is not a module.ts(2306)
import IndexPage from "./IndexPage";

P.S. 另外、--outFile オプションを通じて 1 つの JS Bundle を生成可能(デフォルトはコンパイルして対応する同名 JS 散ファイルを生成)

三.三斜線指令

6 種類の指令をサポート:

  • ファイル間依存を記述:/// <reference path="./myFile.ts" />、現在のディレクトリ下の myFile.ts を引用
  • (タイプ)宣言依存を記述:/// <reference types="node" />@types/node/index.d.ts タイプ宣言を引用、--types オプションに対応
  • 明示的に内蔵(タイプ)ライブラリファイルを引用:/// <reference lib="es2015" /> または /// <reference lib="es2017.string" />、内蔵(タイプ)ライブラリ lib.es2015.d.ts または lib.es2017.string.d.ts を引用、--lib コンパイルオプションに対応
  • デフォルトライブラリを無効:/// <reference no-default-lib="true"/>、コンパイルプロセス中にデフォルトライブラリをロードしない、--noLib コンパイルオプションに対応、同時に現在のファイルをデフォルトライブラリとしてマーク(これにより --skipDefaultLibCheck オプションが該ファイルのチェックをスキップ可能)
  • 現在のモジュールの AMD モジュール名を指定:///<amd-module name="NamedModule"/>、AMD モジュール名を NamedModule に指定
  • AMD モジュール依存を指定(廃止済み):/// <amd-dependency path="legacy/moduleA" name="moduleA"/>legacy/moduleA に依存し、導入モジュール名を moduleA に指定(name 属性はオプション)

P.S. より多くの例は、Triple-Slash Directives を参照

形式的には 3 本のスラッシュで始まるため、三斜線指令(triple-slash directives)と呼ばれます。その中で XML タグはコンパイル指令を表現します。其它の注意事項は以下の通り:

  • ファイル首行(注釈を除く)に出現しないと有効ではありません。つまり、1 本の三斜線指令の前に単行注釈、複数行注釈または其它の三斜線指令のみ出現可能
  • /// <amd-dependency /> 指令は廃止済み、import "moduleName"; で代替
  • --noResolve オプションを指定すると、すべての /// <reference path="..." /> 指令を無視(これらのモジュールを導入しない)

作用上、/// <reference path="..." /> は CSS 中の @import に類似(--outFile オプションを指定する時、モジュール統合順序は path reference 指令順序と一致)

実装上、前処理段階で深度優先ですべての三斜線指令を解析し、指定されたファイルをコンパイルプロセスに追加

P.S. 其它の位置に出現する三斜線指令は普通の単行注釈として扱われ、エラーにはなりませんが、無効(コンパイラは認識しない)

四.エイリアス

名前空間はネストをサポートするため、深層ネストの状況が発生する可能性があります:

namespace Shapes {
  export namespace Polygons {
    export class Triangle { }
    export class Square { }
  }
}

此时エイリアスを通じてモジュール引用を簡素化可能:

import P = Shapes.Polygons;
import Triangle = Shapes.Polygons.Triangle;
let sq = new P.Square();
let triangle = new Triangle();

// コンパイル後
var P = Shapes.Polygons;
var Triangle = Shapes.Polygons.Triangle;
var sq = new P.Square();
var triangle = new Triangle();

難なく発見、ここの importvar の構文糖:

This is similar to using var, but also works on the type and namespace meanings of the imported symbol.

したがって値にエイリアスを付ける時に新しい参照を作成

Importantly, for values, import is a distinct reference from the original symbol, so changes to an aliased var will not be reflected in the original variable.

例えば:

namespace NS {
  export let x = 1;
}
import x = NS.x;
import y = NS.x;
(x as any) = 2;
y === 1;    // true

// コンパイル後
var NS;
(function (NS) {
    NS.x = 1;
})(NS || (NS = {}));
var x = NS.x;
var y = NS.x;
x = 2;
y === 1; // true

P.S. import q = x.y.z エイリアス構文は名前空間にのみ適用、右側は必ず namespace アクセスである必要があります

五.namespacemodule

TypeScript 1.5 之前只有 module 关键字,不区分内部模块(internal modules)与外部模块(external modules),二者都通过 module 关键字来定义。后来清晰起见,新增 namespace 关键字表示内部模块

namespace keyword から引用)

簡言之、キーワード modulenamespace は構文上で完全に等価、例えば:

namespace Shape.Rectangle {
  export let a;
  export let b;
  export function getArea() { return a * b; }
}

module Shape.Rectangle {
  export let a;
  export let b;
  export function getArea() { return a * b; }
}

のコンパイル結果はどちらも:

var Shape;
(function (Shape) {
  var Rectangle;
  (function (Rectangle) {
    function getArea() { return Rectangle.a * Rectangle.b; }
    Rectangle.getArea = getArea;
  })(Rectangle = Shape.Rectangle || (Shape.Rectangle = {}));
})(Shape || (Shape = {}));

namespace で古い module を代替するのは混乱を避けるためだけ、または ES Module、AMD、UMD などの Module 概念(いわゆる外部モジュール)に道を譲るため。なぜならもし module キーワードを独占し、実際に定義しているのが Module ではなく Namespace なら、非常に人を混乱させることだから

六.モジュールと名前空間

内部モジュールと外部モジュール

つまり:

  • 内部モジュール:つまり名前空間、namespace または module キーワードで宣言
  • 外部モジュール:つまりモジュール(ES Module、CommonJS、AMD、UMD など)、特別に宣言する必要はなく、(import または export を含む)ファイル即モジュール

外部モジュールは簡単に外部ファイル中のモジュールと理解可能。なぜなら同一ファイル中で複数の異なる namespace または module(つまり内部モジュール)を定義可能だが、複数の ES Module を定義できないから

P.S. 畢竟名前空間は実質的に IIFE で、モジュールローダーと関係なく、ファイル即モジュールのロードメカニズム制約が存在しない

概念差異

概念上、TypeScript は ES Module 規範(ファイル即モジュール)に従い、コンパイルを通じて CommonJS、AMD、UMD などのモジュール形式を出力

而名前空間は JavaScript 中の [モジュールパターン](/articles/モジュールパターン-javascript デザインパターン 2/) に由来し、旧時代の産物とみなせます。使用を推奨しません([モジュールタイプを宣言](/articles/モジュール-typescript ノート 13/#articleHeader7) する場合を除く)

ロードメカニズム差異

モジュール導入メカニズム上、名前空間は三斜線指令を通じて導入する必要があり、ソースコード埋め込みに相当(CSS 中の @import に類似)、現在のスコープに追加の変数を導入

P.S. 単一ファイル Bundle にパックしない場合、ランタイムでこれらの三斜線指令を通じて導入された依存を導入する必要があります(例えば <script> タグを通じて)

而モジュールは import/require などの方式を通じて導入され、呼び出し側が変数を通じてそれを引用するかどうかを決定し、現在のスコープに積極的に影響を与えません

P.S. import "module-name"; 構文はモジュール(の副作用)のみ導入し、モジュールを引用してアクセスしない、詳細は import を参照

最佳実践

モジュールと名前空間の使用上、いくつかの実践経験があります:

  • 名前空間ネストレベルを減少、例えば静的メソッドのみを持つ class は通常不必要、モジュール名で十分にセマンティクスを表現
  • モジュールが 1 つの API のみ暴露する場合、export default の方がより適切、導入がより便利、而且呼び出し側は API 名を気にする必要がない
  • 複数の API を暴露する場合、すべて直接 export(数量が多すぎる場合、[モジュールオブジェクトを導入](/articles/module-es6 ノート 13/#articleHeader5)、例えば import * as largeModule from 'SoLargeModule'
  • re-export を通じて現有モジュールを拡張、例えば export as
  • モジュール内で名前空間を使用しない、なぜならモジュール本就に論理構造(ファイルディレクトリ構造)とモジュールスコープを持ち、名前空間はより多くのメリットを提供できない

参考資料

コメント

コメントはまだありません

コメントを書く