モジュール
用語に関する注意:
TypeScript1.5において用語の変更における重要な注意事項があります。
ECMAScript 2015の用語に合わせ、現在では、
内部モジュール("Internal modules")は"namespaces"となり、
外部モジュール("External modules")は単純に"modules"となりました。
具体的には、module X {
は、namespace X {
と書くのが好ましいと言えます。
- イントロダクション
- export
- import
- デフォルトexport
- export = と import = require()
- モジュール用のコード生成
- シンプルな例
- オプションによるモジュール読み込みと、その他の最新のローディング・シナリオ
- 別のJavaScriptライブラリと一緒に動作させるには
- モジュール組み立てのガイダンス
イントロダクション
ECMAScript 2015から、JavaScriptがモジュールの概念を持つようになりました。 TypeScriptもこの概念を共有します。
モジュールはグローバルスコープでは無く、それら自身のスコープ内で実行されます。 これは、モジュール内で定義された変数・関数・クラス・その他が、 exportの形式のいずれかを使用して、 明示的にexportをしない限り、モジュール外から見ることが出来ないことを意味します。 また、異なるモジュールからexportされた変数・関数・インターフェース・その他のものを使用するには、 importの形式のいずれかを使用してimportする必要があります。
モジュールは宣言型であり、モジュール間の関係はファイル層でのimportとexportによって指定されます。
モジュールはモジュールローダーを使用しして、別のモジュールをimportします。 実行時のモジュールローダーは位置の特定と、 自身の実行前に依存性のある全てのモジュールの実行に責任を負います。 JavaScriptでよく知られているモジュールローダーに、 Node.jsのためのCommonJSモジュールローダーと、 Webアプリケーションのためのrequire.jsがあります。
TypeScriptではECMAScript 2015と同じように、 最上段にimportまたはexportが含まれるファイルがモジュールであると考えられます。
export
宣言をexport
宣言(変数、関数、クラス、型のエイリアス、またはインターフェースのような)はexport
キーワードを追加することで、
エクスポートされます。
export interface StringValidator {
isAcceptable(s: string): boolean;
}
export const numberRegexp = /^[0-9]+$/;
export class ZipCodeValidator implements StringValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
export文
export文は、名前変更が必要な場合に手軽にそれを行うことが出来ます。 そのため、下記のように書くことが可能です。
class ZipCodeValidator implements StringValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
export { ZipCodeValidator };
export { ZipCodeValidator as mainValidator };
再export
別のモジュールを拡張したり、それらの機能を部分的に使用したいということがよくあります。 再エクスポートはローカルでimportせず、ローカル変数を取り入れます。
export class ParseIntBasedZipCodeValidator {
isAcceptable(s: string) {
return s.length === 5 && parseInt(s).toString() === s;
}
}
// 元のvalidatorを名前を変更してexport
export {ZipCodeValidator as RegExpBasedZipCodeValidator} from "./ZipCodeValidator";
モジュールは1つまたは複数のモジュールをラップ可能で、
export * from "module"
文法を使用してそれら全てのexportを結合することができます。
export * from "./StringValidator"; // exports interface 'StringValidator'
export * from "./LettersOnlyValidator"; // exports class 'LettersOnlyValidator'
export * from "./ZipCodeValidator"; // exports class 'ZipCodeValidator'
import
importの仕組みもexport同様に簡単です。 exportされた宣言のimportは、下記のimport形式のいずれかによって行われます。
モジュールから単一のexportをimport
import { ZipCodeValidator } from "./ZipCodeValidator";
let myValidator = new ZipCodeValidator();
名前を変更してimportします。
import { ZipCodeValidator as ZCV } from "./ZipCodeValidator";
let myValidator = new ZCV();
全てのモジュールを単一の変数にimportし、使用する際にはモジュールのexportにアクセス
import * as validator from "./ZipCodeValidator";
let myValidator = new validator.ZipCodeValidator();
副作用しかないモジュールのインポート
お薦めできない方法ですが、あるモジュールを他のモジュールで使用することが出来るglobal上に設定します。 これらのモジュールはexportを持たないか、使用する側がこれらのexportに関与しない可能性があります。 importするには次のようにします。
import "./my-module.js";
デフォルトexport
各モジュールは、任意でdefault
によるexportを行うことが可能です。
デフォルトexportをするにはdefault
キーワードを付けます。
また、各モジュール毎でデフォルトexportすることが出来るのは1つだけになります。
default
エクスポートは、異なるimport形式を使用してインポートされます。
デフォルトexportは非常に手軽で便利です。
例えば、jQueryはjQuery
または$
のデフォルトexportを持つライブラリで、
我々はimportするのにも、$
またはjQuery
の名前を使用していることでしょう。
declare let $: jQuery;
export default $;
import $ from "jQuery";
$("button.continue").html( "Next Step..." );
クラスと関数宣言は、デフォルトのexportとして直接書くことが可能になっています。 デフォルトexportするクラスと関数の宣言への名付けは、任意になります。
export default class ZipCodeValidator {
static numberRegexp = /^[0-9]+$/;
isAcceptable(s: string) {
return s.length === 5 && ZipCodeValidator.numberRegexp.test(s);
}
}
import validator from "./ZipCodeValidator";
let myValidator = new validator();
または
const numberRegexp = /^[0-9]+$/;
export default function (s: string) {
return s.length === 5 && numberRegexp.test(s);
}
import validate from "./StaticZipCodeValidator";
let strings = ["Hello", "98052", "101"];
// importした関数を使用して検証
strings.forEach(s => {
console.log(`"${s}" ${validate(s) ? " matches" : " does not match"}`);
});
デフォルトのexportは、値だけに適用することも可能です。
export default "123";
import num from "./OneTwoThree";
console.log(num); // "123"
export = と import = require()
CommonJSとAMDはどちらも、一般的にはモジュールからexportされた全てのものをexportするというコンセプトを持ちます。
それ以外にも、exportしたオブジェクトをカスタム・シングルオブジェクトに置換するサポートも行います。
デフォルトexportはこの挙動を置き換えるように意図されていますが、2つを両立することは出来ません。
TypeScriptは伝統的なCommonJSとAMDワークフローのexport =
モデルをサポートします。
export =
文法は、モジュールからexportされた単一のオブジェクトを指し示します。
これは、クラス、インターフェース、名前空間、関数、またはenumのいずれかを指定することが可能です。
export =
が使用されたモジュールのimportをするには、
TypeScript特有のimport let = require("module")
が使用されなければいけません。
let numberRegexp = /^[0-9]+$/;
class ZipCodeValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
export = ZipCodeValidator;
import zip = require("./ZipCodeValidator");
// Some samples to try
let strings = ["Hello", "98052", "101"];
// Validators to use
let validator = new zip();
// Show whether each string passed each validator
strings.forEach(s => {
console.log(`"${ s }" - ${ validator.isAcceptable(s) ? "matches" : "does not match" }`);
});
モジュール用のコード生成
コンパイル時に指定されるモジュール対象に応じて、
コンパイラはNode.js(CommonJS)、
require.js (AMD)、
isomorphic (UMD)、
SystemJS、
ECMAScript 2015 ネイティブモジュールのモジュール読み込みシステムに適したコードを生成します。
define
、require
、register
の呼び出しが生成されたコードで何をしているかの詳細については、
各モジュールローダーのドキュメントを参照してください。
下記のシンプルな例は、importとexportがどのようにモジュール読み込みコードに変換されるのかを表しています。
import m = require("mod");
export let t = m.something + 1;
define(["require", "exports", "./mod"], function (require, exports, mod_1) {
exports.t = mod_1.something + 1;
});
var mod_1 = require("./mod");
exports.t = mod_1.something + 1;
(function (factory) {
if (typeof module === "object" && typeof module.exports === "object") {
var v = factory(require, exports); if (v !== undefined) module.exports = v;
}
else if (typeof define === "function" && define.amd) {
define(["require", "exports", "./mod"], factory);
}
})(function (require, exports) {
var mod_1 = require("./mod");
exports.t = mod_1.something + 1;
});
System.register(["./mod"], function(exports_1) {
var mod_1;
var t;
return {
setters:[
function (mod_1_1) {
mod_1 = mod_1_1;
}],
execute: function() {
exports_1("t", t = mod_1.something + 1);
}
}
});
import { something } from "./mod";
export var t = something + 1;
シンプルな例
下記では、我々はValidatorの実装を各モジュールから名前を付けられた単一のexportのみを先の例を使用して統合しています。
コンパイルするには、コマンドライン上でモジュール対象を指定しなければいけません。
対象をNode.jsとする場合は、--module commonjs
を、
require.jsとする場合は--module amd
を使用します。
例えば、
tsc --module commonjs Test.ts
これがコンパイルされると、各モジュールは個別の.js
ファイルになります。
参照タグと同様に、コンパイラはimport
文に従って依存ファイルをコンパイルします。
export interface StringValidator {
isAcceptable(s: string): boolean;
}
import { StringValidator } from "./Validation";
const lettersRegexp = /^[A-Za-z]+$/;
export class LettersOnlyValidator implements StringValidator {
isAcceptable(s: string) {
return lettersRegexp.test(s);
}
}
import { StringValidator } from "./Validation";
const numberRegexp = /^[0-9]+$/;
export class ZipCodeValidator implements StringValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
import { StringValidator } from "./Validation";
import { ZipCodeValidator } from "./ZipCodeValidator";
import { LettersOnlyValidator } from "./LettersOnlyValidator";
// Some samples to try
let strings = ["Hello", "98052", "101"];
// Validators to use
let validators: { [s: string]: StringValidator; } = {};
validators["ZIP code"] = new ZipCodeValidator();
validators["Letters only"] = new LettersOnlyValidator();
// Show whether each string passed each validator
strings.forEach(s => {
for (let name in validators) {
console.log(`"${ s }" - ${ validators[name].isAcceptable(s) ? "matches" : "does not match" } ${ name }`);
}
});
オプションによるモジュール読み込みと、その他の最新のローディング・シナリオ
あるケースでは、特定の条件下でのみモジュールを読み込みたいことがあるかもしれません。 TypeScriptでは、下記でお見せしている、これを実装するためのパターンと、 別の最新の型の保証を損なうこと無くモジュール・ローダーを動的に実行させるローディング・シナリオを使用することが可能です。
コンパイラは出力されたJavaScript内で各モジュールが使用されたか否かを検知します。
もしモジュール識別で型アノテーションの部分のみで敷きが無いと判断された場合、
モジュールへのrequire
呼び出しは発生しません。
この使用されない参照の省略は最適化のパフォーマンスを向上させ、
それらのモジュールの任意による読み込みも可能にします。
このパターンのコアとなる考えは、import id = require("...")
文がモジュールによって公開された型にアクセス出来るようにしてくれることにあります。
モジュール・ローダーは後述する例のように、その後にブロックがあれば、(require
を通して)動的に実行されます。
これは参照省略の最適化にも影響するため、モジュールは必要とされる場合にのみ読み込まれます。
このパターンを動作させるために、import
を介したシンボルの定義が、
型を定義した範囲でのみ使用されることが重要になります。
(つまり、その範囲に入ることが無ければ、JavaScriptへ出力されないことになります)
型の保証を保持するために、我々はtypeof
を使用することができます。
typeof
キーワードは型が影響する範囲で使用される場合に、値の型を作り出します。
このケースでは、それがモジュールの型に該当します。
declare function require(moduleName: string): any;
import { ZipCodeValidator as Zip } from "./ZipCodeValidator";
if (needZipValidation) {
let ZipCodeValidator: typeof Zip = require("./ZipCodeValidator");
let validator = new ZipCodeValidator();
if (validator.isAcceptable("...")) { /* ... */ }
}
declare function require(moduleNames: string[], onLoad: (...args: any[]) => void): void;
import { ZipCodeValidator as Zip } from "./ZipCodeValidator";
if (needZipValidation) {
require(["./ZipCodeValidator"], (ZipCodeValidator: typeof Zip) => {
let validator = new ZipCodeValidator();
if (validator.isAcceptable("...")) { /* ... */ }
});
}
declare const System: any;
import { ZipCodeValidator as Zip } from "./ZipCodeValidator";
if (needZipValidation) {
System.import("./ZipCodeValidator").then((ZipCodeValidator: typeof Zip) => {
var x = new ZipCodeValidator();
if (x.isAcceptable("...")) { /* ... */ }
});
}
別のJavaScriptライブラリと一緒に動作させるには
TypeScriptで書かれていないライブラリの型を記述するためには、 ライブラリから公開されているAPIをdeclare(宣言)する必要があります。
一般的には、これらは.d.ts
ファイル内で定義されます。
もし、C/C++に親しんでいれば、.hファイルを思い浮かべるでしょう。
幾つかの例を見ていきましょう。
Ambient Modules
Node.jsでは、ほとんどのタスクは1つ以上のモジュールの読み込みによって完遂されます。
最上層で宣言をexportした自身の.d.ts
ファイルで、各モジュールの宣言を行うことが可能ですが、
1つの巨大な.d.ts
ファイルにそれらを書いてしまう方がより便利です。
これを行うために、我々はアンビエント(ambient)名前空間に似たような構成を使用しますが、
module
キーワードを使用して、遅延importを利用可能にするモジュール名をダブルコーテーションで囲みます。
下記はその一例になります。
declare module "url" {
export interface Url {
protocol?: string;
hostname?: string;
pathname?: string;
}
export function parse(urlStr: string, parseQueryString?, slashesDenoteHost?): Url;
}
declare module "path" {
export function normalize(p: string): string;
export function join(...paths: any[]): string;
export var sep: string;
}
これで、/// <reference> node.d.ts
と、
その後のimport url = require("url");
を使用したモジュール読み込みが可能になりました。
/// <reference path="node.d.ts"/>
import * as URL from "url";
let myUrl = URL.parse("http://www.typescriptlang.org");
モジュール組み立てのガイダンス
Export as close to top-level as possible
あなたのモジュール使用者が、あなたがexportするものを使用する際に、可能な限り負担を感じないようにする必要があります。 入れ子になった多くの階層を追加すると煩雑になる傾向があるため、 あなたは構造化について慎重に検討する必要があります。
モジュールから名前空間をexportすることは、多数の入れ子の層を追加する一例と言えるでしょう。 名前空間は時にはそういった用途を持つことがありますが、 モジュールを使用する際には間接的に余計な層を追加することになります。 これは、使用者にとって問題になりやすく、不要であることがほとんどです。
exportされたクラス内の静的(static)メソッドも、 クラス自身に入れ子の層が追加されるため、同様の問題を持ちます。 明確に有用な方法をもって表現や意図を高める目的がない限りは、 単純にヘルパー関数をexportすることを検討してください。
もし、単一のクラスまたは関数のみをexportするのであれば、export default
を使用してください。
"トップに近い層でのexport"は、あなたのモジュール使用者の負担を減らすので、積極的にデフォルトexportを取り入れていきましょう。 もし、モジュールの主たる目的が、1つの特定のexportに収容することであれば、 あなたはそれをデフォルトexportとしてexportすることを検討するべきです。 これはimportとそれを実際に使う場面の両方で、少しだけ簡潔にしてくれる傾向があります。 下記は、その例になります。
export default class SomeType {
constructor() { ... }
}
export default function getThing() { return "thing"; }
import t from "./MyClass";
import f from "./MyFunc";
let x = new t();
console.log(f());
こうすることで使用者にとって、大変使いやすいものになるでしょう。
使用者は型が何であれ、好きな名前(このケースではt
)を付けることができ、
オブジェクトを特定するために過度なドットを打つ必要もありません。
もし、複数のオブジェクトをexportする場合は、それら最上層にそれらを配置してください。
export class SomeType { /* ... */ }
export function someFunc() { /* ... */ }
importは次のように行います。
importされる名前を明示的に列挙
import { SomeType, someFunc } from "./MyThings";
let x = new SomeType();
let y = someFunc();
多くのものをimportする場合に、名前空間importパータンを使用
//
export class Dog { ... }
export class Cat { ... }
export class Tree { ... }
export class Flower { ... }
import * as myLargeModule from "./MyLargeModule.ts";
let x = new myLargeModule.Dog();
継承のための再export
モジュール機能の継承が必要になるケースがたびたび発生します。 CommonJSのパターンでは、jQueryがそうするような方法で拡張によって元のオブジェクトを補強します。 前に述べたように、モジュールはグローバルな名前空間オブジェクトが行うようなマージはしません。 お薦めの解決方法は、元のオブジェクトを変更すること無く、 新しい機能を提供する新しいエンティティ(存在)をexportすることです。
シンプルな計算機の実装が定義された、Calculator.ts
モジュールを例に考えてみましょう。
また、モジュールは入力用の文字列のリストを渡すことで最終的な結果を書き出す、
計算機能をテストするヘルパー関数もexportします。
export class Calculator {
private current = 0;
private memory = 0;
private operator: string;
protected processDigit(digit: string, currentValue: number) {
if (digit >= "0" && digit <= "9") {
return currentValue * 10 + (digit.charCodeAt(0) - "0".charCodeAt(0));
}
}
protected processOperator(operator: string) {
if (["+", "-", "*", "/"].indexOf(operator) >= 0) {
return operator;
}
}
protected evaluateOperator(operator: string, left: number, right: number): number {
switch (this.operator) {
case "+": return left + right;
case "-": return left - right;
case "*": return left * right;
case "/": return left / right;
}
}
private evaluate() {
if (this.operator) {
this.memory = this.evaluateOperator(this.operator, this.memory, this.current);
}
else {
this.memory = this.current;
}
this.current = 0;
}
public handelChar(char: string) {
if (char === "=") {
this.evaluate();
return;
}
else {
let value = this.processDigit(char, this.current);
if (value !== undefined) {
this.current = value;
return;
}
else {
let value = this.processOperator(char);
if (value !== undefined) {
this.evaluate();
this.operator = value;
return;
}
}
}
throw new Error(`Unsupported input: '${char}'`);
}
public getResult() {
return this.memory;
}
}
export function test(c: Calculator, input: string) {
for (let i = 0; i < input.length; i++) {
c.handelChar(input[i]);
}
console.log(`result of '${input}' is '${c.getResult()}'`);
}
下記は、このtest
関数を使用した計算機のシンプルなテストを行っています。
import { Calculator, test } from "./Calculator";
let c = new Calculator();
test(c, "1+2*33/11="); // prints 9
これを継承して、底(base)が10より大きい数値の入力のサポートが追加されたProgrammerCalculator.ts
を作成してみましょう。
import { Calculator } from "./Calculator";
class ProgrammerCalculator extends Calculator {
static digits = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"];
constructor(public base: number) {
super();
if (base <= 0 || base > ProgrammerCalculator.digits.length) {
throw new Error("base has to be within 0 to 16 inclusive.");
}
}
protected processDigit(digit: string, currentValue: number) {
if (ProgrammerCalculator.digits.indexOf(digit) >= 0) {
return currentValue * this.base + ProgrammerCalculator.digits.indexOf(digit);
}
}
}
// Calculatorとして拡張された新しい計算機をexport
export { ProgrammerCalculator as Calculator };
// 同様にヘルパー関数もexport
export { test } from "./Calculator";
新しいモジュールであるProgrammerCalculator
は元のCalculator
モジュールと同様のAPI構造をexportしますが、
元のモジュールのオブジェクトを変更するようなことはしていません。
下記はProgrammerCalculator
のためのテストです。
import { Calculator, test } from "./ProgrammerCalculator";
let c = new Calculator(2);
test(c, "001+010="); // 3を出力
モジュール内でnamespace(名前空間)は使用しないでください
モジュールベースの構造に移行する際の一般的な傾向として、まず追加された名前空間の層にexportをラップします。 モジュールは独自のスコープを持ち、また唯一exportされた宣言がモジュール外からアクセスされることになります。 モジュールを扱う際はこの点を考慮して、namespaceを使用するのであれば、提供する値の数は極力少なくしてください。
構造化の点では、名前空間はグローバルスコープ上で論理的に関連性を持つオブジェクトとと型をグループ化するのに便利です。 例えばC#では、System.Collectionsでコレクション型の全てを見つけることができます。 自分達の作成した型を階層化されたnamespaceに構造化することで、 これらの型が使用者にとって見つけやすい(扱いやすい)ものにすることができます。
一方で、モジュールはファイルシステム上でも、必然的にモジュール用のファイルとして提供されることが多いでしょう。 パスとファイル名によってこれを解決する必要があるため、論理的な構造化スキームの存在が欠かせません。 我々はリストのモジュールが含まれた/collections/generic/フォルダを持つことができます。
namespaceはグローバルスコープ上で名前による衝突を避けるために欠かせないものです。
例えば、あなたがMy.Application.Customer.AddForm
とMy.Application.Order.AddForm
の、
同じ名前ではあるもののnamespaceが異なる2つの型を持つとします。
モジュールとしての問題は何もありませんが、
モジュール内で同じ名前の2つのオブジェクトを持つ妥当なな理由もありません。
使用する側からすれば、選んだ名前をモジュール参照のために使用するため、
偶発的な名前の衝突が発生することはありえません。
モジュールと名前空間に関する詳しい内容については、名前空間とモジュールを参照してください。
注意事項
下記は、モジュールを構築する際に注意するべきことの一覧になります。 ファイルが下記の内容のいずれかに該当していた場合、 外部モジュールにnamespaceを適用していないことを厳密にチェック(ダブルチェック)してください。
-
ファイルの最上層の宣言がnamespaceの
Foo { ... }
のみをexportしている(Foo
を削除して階層を"上げて"ください)。 -
ファイルが単一のexprotクラスまたはexport関数を持つ(
export default
の使用も考慮)。 -
複数のファイルが最上層で同じ
export namespace Foo {
を持つ(これらが一つのFooに結合されると考えないないでください!)。
© https://github.com/Microsoft/TypeScript-Handbook
このページは、ページトップのリンク先のTypeScript-Handbook内のページを翻訳した内容を基に構成されています。 下記の項目を確認し、必要に応じて公式のドキュメントをご確認ください。 もし、誤訳などの間違いを見つけましたら、 @tomofまで教えていただければ幸いです。
- ドキュメントの情報が古い可能性があります。
- "訳注:"などの断わりを入れた上で、日本人向けの情報やより分かり易くするための追記を行っている事があります。