TypeScript Handbook を読み進めていく第十三回目。
- Basic Types
- Variable Declarations
- Interfaces
- Classes
- Functions
- Generics
- Enums
- Type Inference
- Type Compatibility
- Advanced Types
- Symbols
- Iterators and Generators
- Modules (今ココ)
- Namespaces
- Namespaces and Modules
- Module Resolution
- Declaration Merging
- JSX
- Decorators
- Mixins
- Triple-Slash Directives
- Type Checking JavaScript Files
Modules
Introduction
ECMAScript 2015 からモジュールという考え方が導入されましたが、TypeScript も同様の考え方を持っています。
モジュールは独自のスコープの中で実行されるため、いずれかの export
形式 に従って明示的にエクスポートしない限り、モジュール外から参照することはできません。
逆に、他のモジュールからエクスポートされたものを使用するためには、いずれかの import
形式 に従ってインポートする必要があります。
モジュールは宣言的であり、モジュール間の依存関係はファイル単位で記述します。
各モジュールはモジュールローダーによってファイルの場所と依存関係の解決が行われます。
JavaScript でよく知られているモジュールローダーとしては、Node.js で用いられている CommonJS や Web アプリケーションでよく使用される require.js が挙げられます。
TypeScript では ECMAScript 2015 と同様に、トップレベルで import
、export
が使用されているファイルはすべてモジュールとして扱います。
逆に、トップレベルの import
、export
宣言を持たないファイルは、内容がすべてグローバルスコープに宣言されたものとして扱われます。(そのため、その内容は他モジュールからも参照できます)
Export
Exporting a declaration
変数、関数、クラス、型エイリアス、インタフェースといった、任意の宣言を 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 statements
別名でエクスポートする時には export 文が便利です。
class ZipCodeValidator implements StringValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
export { ZipCodeValidator }; // 'ZipCodeValidator' としてエクスポート
export { ZipCodeValidator as mainValidator }; // 'mainValidator' としてエクスポート
Re-exports
他のモジュールを拡張したり、一部の機能のみを再利用することはよくありますが、モジュールを再エクスポートしても、再エクスポートを行っているモジュール内に元のモジュールがインポートされたり、ローカル変数に格納されることはありません。
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"
構文を使用して複数のモジュールをまとめてエクスポートするモジュールを作成することも可能です。
export * from "./StringValidator"; // 'StringValidator' インタフェースをエクスポート
export * from "./LettersOnlyValidator"; // 'LettersOnlyValidator' クラスをエクスポート
export * from "./ZipCodeValidator"; // 'ZipCodeValidator' クラスをエクスポート
Import
Import a single export from a module
モジュールのインポートはモジュールのエクスポートと同じくらい簡単です。
import { ZipCodeValidator } from "./ZipCodeValidator";
let myValidator = new ZipCodeValidator();
from
に/
が含まれるかどうかで外部ライブラリと自身のコードを区別しているようだ。
なので、同フォルダのモジュールをインポートする場合でも./
は必須。
別名でインポートすることも可能です。
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
import * as validator from "./ZipCodeValidator";
let myValidator = new validator.ZipCodeValidator();
エクスポートされているものを
*
でまとめてインポート
Import a module for side-effects only
推奨はしていませんが、モジュールの中にはグローバルな状態を変更するものもあります。
そのようなモジュールは何もインポートするものがないでしょうから、以下のようにインポートします。
import "./my-module.js";
Default exports
各モジュールは必要に応じてデフォルトエクスポートが可能です。
デフォルトエクスポートは 1 モジュールにつき一回だけ宣言することが可能で、default
キーワードを使用して宣言します。
デフォルトエクスポートは非常に便利で、例えば JQuery であれば jQuery
や $
をデフォルトエクスポートしています。
declare let $: JQuery;
export default $;
import $ from "JQuery";
$("button.continue").html( "Next Step..." );
クラスや関数であれば宣言時に直接デフォルトエクスポートにすることが可能です。
デフォルトエクスポートするクラスや関数は必ずしも名前を付ける必要はありません。
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"];
// バリデート関数を使用する
strings.forEach(s => {
console.log(`"${s}" ${validate(s) ? " matches" : " does not match"}`);
});
どうせ
import
するときに利用者が好きな名前を指定するから、モジュールの提供側で名前を付ける必要はないというわけね。
と言いつつ、何のクラス/関数なのかが分からなくなるので名前は付けとくべきと思う。
値そのものをデフォルトエクスポートすることも可能です。
export default "123";
import num from "./OneTwoThree";
console.log(num); // "123"
export =
and import = require()
TypeScript は従来の CommonJS や AMD で用いられていた export =
構文をサポートしています。
export =
構文では単一のオブジェクトをエクスポートすることができます。
export =
を使用したモジュールをインポートする時には TypeScript 独自の構文である import 〜 = require("module")
を使用する必要があります。
let numberRegexp = /^[0-9]+$/;
class ZipCodeValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
export = ZipCodeValidator;
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)、 SystemJS、 ECMAScript 2015 (ES6) に準拠したコードが生成されます。
各モジュールターゲットの生成コードの例を以下に示します。
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;
いつもどのモジュールターゲットを選べばいいのか悩む…。
複数ファイルの結合を TypeScript で行う (--outFile
オプションを使用する) 場合はAMD
とSystem
のどちらかを選択する必要があるらしいけど、Webpack とかを使うのであればどれでも良いのかな?
Simple Example
この節では、これまでの例に登場したバリデータを各モジュールごとにひとつの名前付きエクスポートとして宣言したものとして説明します。
モジュールをコンパイルするためにはコマンドラインでモジュールターゲットを指定する必要があります。
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";
// バリデーション対象
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 ということっぽい気がする。
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 * as Zip from "./ZipCodeValidator";
if (needZipValidation) {
require(["./ZipCodeValidator"], (ZipCodeValidator: typeof Zip) => {
let validator = new ZipCodeValidator.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("...")) { /* ... */ }
});
}
Working with Other JavaScript Libraries
TypeScript で書かれていないライブラリを使用するにあたり、そのライブラリの提供している API の宣言が必要です。
このように実装を伴わない宣言のことを "ambient" と呼んでおり、一般的に .d.ts
ファイルに宣言します。
もしも C/C++ に馴染みがあるのであれば、.h
ファイルのようなものと考えてもらって結構です。
Ambient Modules
Node.js では、様々なモジュールを使用して処理を行うことが多いです。
それらのモジュールを個々の .d.ts
ファイルに定義することも可能ですが、まとめてひとつの .d.ts
ファイルに定義した方が便利でしょう。
そのためには module
キーワードとクォートされた名前を使用します。
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"
を使ってモジュールを読み込みます。
/// <reference path="node.d.ts"/>
import * as URL from "url";
let myUrl = URL.parse("http://www.typescriptlang.org");
Shorthand ambient modules
新しいモジュールを使用する時にわざわざ宣言を書くのが手間な場合、簡略記法で記述することも可能です。
declare module "hot-new-module";
簡略記法で記述したモジュールからインポートしたものはすべて any
型として扱われます。
import x, {y} from "hot-new-module";
x(y);
Wildcard module declarations
SystemJS や AMD 等、一部のモジュールローダーでは JavaScript 以外のものをインポートすることができます。
これを実現するために、一般的には特別な接頭語や接尾語が用いられますが、ワイルドカードモジュール宣言を用いることでこれに対応することが可能です。
declare module "*!text" {
const content: string;
export default content;
}
// 別の宣言方法
declare module "json!*" {
const value: any;
export default value;
}
このように宣言すると、"*!text"
や "json!*"
にマッチするものをインポートすることができます。
import fileContent from "./xyz.txt!text";
import data from "json!http://example.com/data.json";
console.log(data, fileContent);
個々のモジュールを宣言しなくても、事前に定義した宣言を使い回すことができる、というイメージかな
UMD modules
いくつかのライブラリは複数のモジュールローダーに対応していたり、モジュールローダーを使用しなくても使用できるように設計されていますが、これは UMD や Isomorphic モジュールとして知られています。
export function isPrime(x: number): boolean;
export as namespace mathLib;
このように宣言されたライブラリであれば、モジュール内でインポートされたものとして使用することが可能です。
import { isPrime } from "math-lib";
isPrime(2);
mathLib.isPrime(2); // エラー: モジュール内からグローバル定義を使用することはできない
逆にスクリプト (インポート/エクスポートを持たないファイル) からはグローバル変数のみを使用できます。
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
モジュールが特定のエクスポートを提供するためだけであれば、それをデフォルトエクスポートにすることを検討してみてください。
デフォルトエクスポートを使用すると、インポート文やモジュールを使うコードを簡単に記述できるようになります。
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());
If you’re exporting multiple objects, put them all at top-level
export class SomeType { /* ... */ }
export function someFunc() { /* ... */ }
無駄に名前空間とかでラッピングするなということね
Explicitly list imported names
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
export class Dog { ... }
export class Cat { ... }
export class Tree { ... }
export class Flower { ... }
import * as myLargeModule from "./MyLargeModule.ts";
let x = new myLargeModule.Dog();
import * as 〜 from ...
が名前空間付きでのインポート文だろう。
こうする理由はクラス名/関数名の重複を避けるためかな。
Re-export to extend
common JS や jQuery の拡張機能のように、モジュールの機能を拡張したくなることはよくあるでしょう。
ですが、モジュールはグローバル名前空間のように マージ することはできません。
その代わりに推奨する方法としては、元のオブジェクトを修正する のではなく 、新しい機能を提供するクラス/関数をエクスポートすることです。
例として、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
関数を使用したテストコードは以下の通りです。
import { Calculator, test } from "./Calculator";
let c = new Calculator();
test(c, "1+2*33/11="); // 9 が出力される
関係ないけど、出力されるのは 7 では?
さて、ここで 10 進数以外も受け取れる ProgrammerCalculator.ts
を実装してみましょう。
新しい ProgrammerCalculator
モジュールでは元の Calculator
と同様のインタフェースの API を提供しますが、それ以外には何も追加しません。
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 のテストコードは以下のようになります。
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 class
かexport function
をひとつだけ宣言している
(export default
を使用することを検討してください) - 複数のファイルで同じように
export namespace Foo {
と宣言している
(それらが同じFoo
名前空間に統合されることはありません!)