LoginSignup
23
19

More than 5 years have passed since last update.

TypeScript Handbook を読む (13. Modules)

Last updated at Posted at 2017-04-29

TypeScript Handbook を読み進めていく第十三回目。

  1. Basic Types
  2. Variable Declarations
  3. Interfaces
  4. Classes
  5. Functions
  6. Generics
  7. Enums
  8. Type Inference
  9. Type Compatibility
  10. Advanced Types
  11. Symbols
  12. Iterators and Generators
  13. Modules (今ココ)
  14. Namespaces
  15. Namespaces and Modules
  16. Module Resolution
  17. Declaration Merging
  18. JSX
  19. Decorators
  20. Mixins
  21. Triple-Slash Directives
  22. Type Checking JavaScript Files

Modules

原文

Introduction

ECMAScript 2015 からモジュールという考え方が導入されましたが、TypeScript も同様の考え方を持っています。

モジュールは独自のスコープの中で実行されるため、いずれかの export 形式 に従って明示的にエクスポートしない限り、モジュール外から参照することはできません。
逆に、他のモジュールからエクスポートされたものを使用するためには、いずれかの import 形式 に従ってインポートする必要があります。

モジュールは宣言的であり、モジュール間の依存関係はファイル単位で記述します。

各モジュールはモジュールローダーによってファイルの場所と依存関係の解決が行われます。
JavaScript でよく知られているモジュールローダーとしては、Node.js で用いられている CommonJS や Web アプリケーションでよく使用される require.js が挙げられます。

TypeScript では ECMAScript 2015 と同様に、トップレベルで importexport が使用されているファイルはすべてモジュールとして扱います。
逆に、トップレベルの importexport 宣言を持たないファイルは、内容がすべてグローバルスコープに宣言されたものとして扱われます。(そのため、その内容は他モジュールからも参照できます)

Export

Exporting a declaration

変数、関数、クラス、型エイリアス、インタフェースといった、任意の宣言を export キーワードによってエクスポートすることが可能です。

Validation.ts
export interface StringValidator {
    isAcceptable(s: string): boolean;
}
ZipCodeValidator.ts
export const numberRegexp = /^[0-9]+$/;

export class ZipCodeValidator implements StringValidator {
    isAcceptable(s: string) {
        return s.length === 5 && numberRegexp.test(s);
    }
}

Export statements

別名でエクスポートする時には export 文が便利です。

TypeScript
class ZipCodeValidator implements StringValidator {
    isAcceptable(s: string) {
        return s.length === 5 && numberRegexp.test(s);
    }
}
export { ZipCodeValidator }; // 'ZipCodeValidator' としてエクスポート
export { ZipCodeValidator as mainValidator }; // 'mainValidator' としてエクスポート

Re-exports

他のモジュールを拡張したり、一部の機能のみを再利用することはよくありますが、モジュールを再エクスポートしても、再エクスポートを行っているモジュール内に元のモジュールがインポートされたり、ローカル変数に格納されることはありません。

ParseIntBasedZipCodeValidator.ts
export class ParseIntBasedZipCodeValidator {
    isAcceptable(s: string) {
        return s.length === 5 && parseInt(s).toString() === s;
    }
}

// 元のバリデータを別名でエクスポートする
export {ZipCodeValidator as RegExpBasedZipCodeValidator} from "./ZipCodeValidator";

ParseIntBasedZipCodeValidator に加えて、別モジュールで宣言されている ZipCodeValidator を (リネームした上で) エクスポートしているから「再エクスポート」の例ということかな。
説明の後半の意味はよく分かってないけど、ZipCodeValidator を明示的にインポートしないと ParseIntBasedZipCodeValidator.ts の中で ZipCodeValidator を使用することはできないということだろう。

必要に応じて、export * from "module" 構文を使用して複数のモジュールをまとめてエクスポートするモジュールを作成することも可能です。

AllValidators.ts
export * from "./StringValidator"; // 'StringValidator' インタフェースをエクスポート
export * from "./LettersOnlyValidator"; // 'LettersOnlyValidator' クラスをエクスポート
export * from "./ZipCodeValidator";  // 'ZipCodeValidator' クラスをエクスポート

Import

Import a single export from a module

モジュールのインポートはモジュールのエクスポートと同じくらい簡単です。

TypeScript
import { ZipCodeValidator } from "./ZipCodeValidator";

let myValidator = new ZipCodeValidator();

from/ が含まれるかどうかで外部ライブラリと自身のコードを区別しているようだ。
なので、同フォルダのモジュールをインポートする場合でも ./ は必須。

別名でインポートすることも可能です。

TypeScript
import { ZipCodeValidator as ZCV } from "./ZipCodeValidator";
let myValidator = new ZCV();

Import the entire module into a single variable, and use it to access the module exports

TypeScript
import * as validator from "./ZipCodeValidator";
let myValidator = new validator.ZipCodeValidator();

エクスポートされているものを * でまとめてインポート

Import a module for side-effects only

推奨はしていませんが、モジュールの中にはグローバルな状態を変更するものもあります。
そのようなモジュールは何もインポートするものがないでしょうから、以下のようにインポートします。

TypeScript
import "./my-module.js";

Default exports

各モジュールは必要に応じてデフォルトエクスポートが可能です。
デフォルトエクスポートは 1 モジュールにつき一回だけ宣言することが可能で、default キーワードを使用して宣言します。

デフォルトエクスポートは非常に便利で、例えば JQuery であれば jQuery$ をデフォルトエクスポートしています。

JQuery.d.ts
declare let $: JQuery;
export default $;
App.ts
import $ from "JQuery";

$("button.continue").html( "Next Step..." );

クラスや関数であれば宣言時に直接デフォルトエクスポートにすることが可能です。
デフォルトエクスポートするクラスや関数は必ずしも名前を付ける必要はありません。

ZipCodeValidator.ts
export default class ZipCodeValidator {
    static numberRegexp = /^[0-9]+$/;
    isAcceptable(s: string) {
        return s.length === 5 && ZipCodeValidator.numberRegexp.test(s);
    }
}
Test.ts
import validator from "./ZipCodeValidator";

let myValidator = new validator();

または以下のようにも宣言できます。

StaticZipCodeValidator.ts
const numberRegexp = /^[0-9]+$/;

export default function (s: string) {
    return s.length === 5 && numberRegexp.test(s);
}
Test.ts
import validate from "./StaticZipCodeValidator";

let strings = ["Hello", "98052", "101"];

// バリデート関数を使用する
strings.forEach(s => {
  console.log(`"${s}" ${validate(s) ? " matches" : " does not match"}`);
});

どうせ import するときに利用者が好きな名前を指定するから、モジュールの提供側で名前を付ける必要はないというわけね。
と言いつつ、何のクラス/関数なのかが分からなくなるので名前は付けとくべきと思う。

値そのものをデフォルトエクスポートすることも可能です。

OneTwoThree.ts
export default "123";
Log.ts
import num from "./OneTwoThree";

console.log(num); // "123"

export = and import = require()

TypeScript は従来の CommonJS や AMD で用いられていた export = 構文をサポートしています。
export = 構文では単一のオブジェクトをエクスポートすることができます。

export = を使用したモジュールをインポートする時には TypeScript 独自の構文である import 〜 = require("module") を使用する必要があります。

ZipCodeValidator.ts
let numberRegexp = /^[0-9]+$/;
class ZipCodeValidator {
    isAcceptable(s: string) {
        return s.length === 5 && numberRegexp.test(s);
    }
}
export = ZipCodeValidator;
Test.ts
import zip = require("./ZipCodeValidator");

// バリデーション対象
let strings = ["Hello", "98052", "101"];

// 使用するバリデータ
let validator = new zip();

// 各文字列がバリデーションを通過したかどうかを表示する
strings.forEach(s => {
  console.log(`"${ s }" - ${ validator.isAcceptable(s) ? "matches" : "does not match" }`);
});

この書き方も可能とはいえ、なるべく ECMAScript 2015 に準拠している import/export の方を使用するべきなんだろうね

Code Generation for Modules

コンパイル時に指定されたモジュールターゲットに応じて、Node.js (CommonJS)、 require.js (AMD)、 isomorphic (UMD)、 SystemJSECMAScript 2015 (ES6) に準拠したコードが生成されます。

各モジュールターゲットの生成コードの例を以下に示します。

SimpleModule.ts
import m = require("mod");
export let t = m.something + 1;
AMD/RequireJS
define(["require", "exports", "./mod"], function (require, exports, mod_1) {
    exports.t = mod_1.something + 1;
});
CommonJS/Node
var mod_1 = require("./mod");
exports.t = mod_1.something + 1;
UMD
(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
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);
        }
    }
});
ECMAScript2015
import { something } from "./mod";
export var t = something + 1;

いつもどのモジュールターゲットを選べばいいのか悩む…。
複数ファイルの結合を TypeScript で行う (--outFile オプションを使用する) 場合は AMDSystem のどちらかを選択する必要があるらしいけど、Webpack とかを使うのであればどれでも良いのかな?

Simple Example

この節では、これまでの例に登場したバリデータを各モジュールごとにひとつの名前付きエクスポートとして宣言したものとして説明します。

モジュールをコンパイルするためにはコマンドラインでモジュールターゲットを指定する必要があります。
Node.js 向けであれば --module commonjs ですし、require.js 向けであれば --module amd です。

コマンド
tsc --module commonjs Test.ts

各モジュールは個別の .js ファイルにコンパイルされます。
参照タグで指定された依存関係に基づき、 import 文に指定された依存ファイルも自動的にコンパイルされます。

Validation.ts
export interface StringValidator {
    isAcceptable(s: string): boolean;
}
LettersOnlyValidator.ts
import { StringValidator } from "./Validation";

const lettersRegexp = /^[A-Za-z]+$/;

export class LettersOnlyValidator implements StringValidator {
    isAcceptable(s: string) {
        return lettersRegexp.test(s);
    }
}
ZipCodeValidator.ts
import { StringValidator } from "./Validation";

const numberRegexp = /^[0-9]+$/;

export class ZipCodeValidator implements StringValidator {
    isAcceptable(s: string) {
        return s.length === 5 && numberRegexp.test(s);
    }
}
Test.ts
import { StringValidator } from "./Validation";
import { ZipCodeValidator } from "./ZipCodeValidator";
import { LettersOnlyValidator } from "./LettersOnlyValidator";

// バリデーション対象
let strings = ["Hello", "98052", "101"];

// 使用するバリデータ
let validators: { [s: string]: StringValidator; } = {};
validators["ZIP code"] = new ZipCodeValidator();
validators["Letters only"] = new LettersOnlyValidator();

// 各文字列がバリデーションを通過したかどうかを表示する
strings.forEach(s => {
    for (let name in validators) {
        console.log(`"${ s }" - ${ validators[name].isAcceptable(s) ? "matches" : "does not match" } ${ name }`);
    }
});

Optional Module Loading and Other Advanced Loading Scenarios

コンパイラは出力した JavaScript の中で各モジュールが使用されているかどうかを判定することができます。
そのため、もしもモジュール識別子が型宣言のみに使用されており、処理の中では使用されていない場合、そのモジュールを読み込むための require 文は出力されません。

つまり、以下の例のように、動的に (require を通じて) モジュールをロードすることが可能というわけです。
ここで重要なことは、インポートしたシンボルは型宣言でのみ使用することです。(JavaScript に出力されるコードの中で使用してはいけません)

モジュールを使いたいのに「JavaScript に出力されるコードの中で使うな」というのは矛盾しているように見えるけど、サンプルコードを見る限り、インポート文とインポートしたモジュールを使うコードを特定のブロック内に押し込めれば OK ということっぽい気がする。

Node.js版
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("...")) { /* ... */ }
}
require.js版
declare function require(moduleNames: string[], onLoad: (...args: any[]) => void): void;

import  * as Zip from "./ZipCodeValidator";

if (needZipValidation) {
    require(["./ZipCodeValidator"], (ZipCodeValidator: typeof Zip) => {
        let validator = new ZipCodeValidator.ZipCodeValidator();
        if (validator.isAcceptable("...")) { /* ... */ }
    });
}
System.js版
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("...")) { /* ... */ }
    });
}

Working with Other JavaScript Libraries

TypeScript で書かれていないライブラリを使用するにあたり、そのライブラリの提供している API の宣言が必要です。

このように実装を伴わない宣言のことを "ambient" と呼んでおり、一般的に .d.ts ファイルに宣言します。
もしも C/C++ に馴染みがあるのであれば、.h ファイルのようなものと考えてもらって結構です。

Ambient Modules

Node.js では、様々なモジュールを使用して処理を行うことが多いです。
それらのモジュールを個々の .d.ts ファイルに定義することも可能ですが、まとめてひとつの .d.ts ファイルに定義した方が便利でしょう。
そのためには module キーワードとクォートされた名前を使用します。

node.d.ts(抜粋)
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");import * as URL from "url" を使ってモジュールを読み込みます。

TypeScript
/// <reference path="node.d.ts"/>
import * as URL from "url";
let myUrl = URL.parse("http://www.typescriptlang.org");

Shorthand ambient modules

新しいモジュールを使用する時にわざわざ宣言を書くのが手間な場合、簡略記法で記述することも可能です。

declarations.d.ts
declare module "hot-new-module";

簡略記法で記述したモジュールからインポートしたものはすべて any 型として扱われます。

TypeScript
import x, {y} from "hot-new-module";
x(y);

Wildcard module declarations

SystemJSAMD 等、一部のモジュールローダーでは JavaScript 以外のものをインポートすることができます。
これを実現するために、一般的には特別な接頭語や接尾語が用いられますが、ワイルドカードモジュール宣言を用いることでこれに対応することが可能です。

TypeScript
declare module "*!text" {
    const content: string;
    export default content;
}
// 別の宣言方法
declare module "json!*" {
    const value: any;
    export default value;
}

このように宣言すると、"*!text""json!*" にマッチするものをインポートすることができます。

TypeScript
import fileContent from "./xyz.txt!text";
import data from "json!http://example.com/data.json";
console.log(data, fileContent);

個々のモジュールを宣言しなくても、事前に定義した宣言を使い回すことができる、というイメージかな

UMD modules

いくつかのライブラリは複数のモジュールローダーに対応していたり、モジュールローダーを使用しなくても使用できるように設計されていますが、これは UMDIsomorphic モジュールとして知られています。

math-lib.d.ts
export function isPrime(x: number): boolean;
export as namespace mathLib;

このように宣言されたライブラリであれば、モジュール内でインポートされたものとして使用することが可能です。

TypeScript
import { isPrime } from "math-lib";
isPrime(2);
mathLib.isPrime(2); // エラー: モジュール内からグローバル定義を使用することはできない

逆にスクリプト (インポート/エクスポートを持たないファイル) からはグローバル変数のみを使用できます。

TypeScript
mathLib.isPrime(2);

export as namespace 〜 するとグローバル変数に格納されるということかな?

Guidance for structuring modules

Export as close to top-level as possible

モジュールの利用者に対し、可能な限り負担を強いないようにするべきです。
ネストを増やし過ぎてしまうとモジュールが使いづらくなってしまうため、モジュールの構造については充分注意を払う必要があります。

無駄にネストを増やしてしまう例として、名前空間のエクスポートが挙げられます。
名前空間が役に立つ場面もあるものの、モジュールを使用する場合には余分なレイヤーを増えることになります。

エクスポートしたクラスの静的メソッドも同様です。
コードや意図が分かりやすくなるのでなければ、単純にヘルパー関数をエクスポートすることを検討してみてください。

If you’re only exporting a single class or function, use export default

モジュールが特定のエクスポートを提供するためだけであれば、それをデフォルトエクスポートにすることを検討してみてください。
デフォルトエクスポートを使用すると、インポート文やモジュールを使うコードを簡単に記述できるようになります。

MyClass.ts
export default class SomeType {
  constructor() { ... }
}
MyFunc.ts
export default function getThing() { return "thing"; }
Consumer.ts
import t from "./MyClass";
import f from "./MyFunc";
let x = new t();
console.log(f());

If you’re exporting multiple objects, put them all at top-level

MyThings.ts
export class SomeType { /* ... */ }
export function someFunc() { /* ... */ }

無駄に名前空間とかでラッピングするなということね

Explicitly list imported names

Consumer.ts
import { SomeType, someFunc } from "./MyThings";
let x = new SomeType();
let y = someFunc();

例の通り、インポートするものを明示しろ (ワイルドカードでインポートするな) ということね

Use the namespace import pattern if you’re importing a large number of things

MyLargeModule.ts
export class Dog { ... }
export class Cat { ... }
export class Tree { ... }
export class Flower { ... }
Consumer.ts
import * as myLargeModule from "./MyLargeModule.ts";
let x = new myLargeModule.Dog();

import * as 〜 from ... が名前空間付きでのインポート文だろう。
こうする理由はクラス名/関数名の重複を避けるためかな。

Re-export to extend

common JS や jQuery の拡張機能のように、モジュールの機能を拡張したくなることはよくあるでしょう。
ですが、モジュールはグローバル名前空間のように マージ することはできません。
その代わりに推奨する方法としては、元のオブジェクトを修正する のではなく 、新しい機能を提供するクラス/関数をエクスポートすることです。

例として、Calculator.ts モジュールを拡張する場合を見てみましょう。
このモジュールでは簡単な計算機の実装と、テスト用ヘルパー関数をエクスポートしています。

Calculator.ts

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 関数を使用したテストコードは以下の通りです。

TestCalculator.ts
import { Calculator, test } from "./Calculator";


let c = new Calculator();
test(c, "1+2*33/11="); // 9 が出力される

関係ないけど、出力されるのは 7 では?

さて、ここで 10 進数以外も受け取れる ProgrammerCalculator.ts を実装してみましょう。
新しい ProgrammerCalculator モジュールでは元の Calculator と同様のインタフェースの API を提供しますが、それ以外には何も追加しません。

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();
        const maxBase = ProgrammerCalculator.digits.length;
        if (base <= 0 || base > maxBase) {
            throw new Error(`base has to be within 0 to ${maxBase} inclusive.`);
        }
    }

    protected processDigit(digit: string, currentValue: number) {
        if (ProgrammerCalculator.digits.indexOf(digit) >= 0) {
            return currentValue * this.base + ProgrammerCalculator.digits.indexOf(digit);
        }
    }
}

// 新しく拡張した計算機を Calculator としてエクスポートする
export { ProgrammerCalculator as Calculator };

// ヘルパー関数もエクスポートする
export { test } from "./Calculator";

ProgrammerCalculator のテストコードは以下のようになります。

TestProgrammerCalculator.ts
import { Calculator, test } from "./ProgrammerCalculator";

let c = new Calculator(2);
test(c, "001+010="); // prints 3

Do not use namespaces in modules

モジュールを使い始めた頃はエクスポートするものを名前空間で囲みたくなることでしょう。
ですが、モジュールではエクスポートしたものしか外部から見えないため、名前空間を使用する意味はほとんどありません。

さらに、同じモジュール内で同じ名前のオブジェクトを定義することはないでしょうし、同名のオブジェクトを持つモジュールがあっても、モジュールを使用する際にはそれぞれのモジュールに別の名前を付けて使用するため、名前の衝突を回避する目的で名前空間を使用する必要はありません。

Red Flags

モジュールの構造に関する注意点を以下に記載します。
あなたの作成するモジュールファイルが以下のいずれかに当てはまる場合、くれぐれもモジュールを名前空間で囲まないようにしてください。

  • トップレベルの宣言が export namespace Foo { ... } となっている
    (Foo を削除し、宣言を一つ上の階層に '引き上げて' ください)
  • export classexport function をひとつだけ宣言している
    (export default を使用することを検討してください)
  • 複数のファイルで同じように export namespace Foo { と宣言している
    (それらが同じ Foo 名前空間に統合されることはありません!)
23
19
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
19