第2章 TypeScriptの基本

第1章「戦闘準備だ!TypeScript!」で述べたとおり、本書ではECMAScriptの文法・仕様についてすべてを解説することはしません。ECMAScriptの知識はどんどん広まってきていますし、最近では知っている人も多い知識になってきました。

この章ではTypeScriptでの基本的な構文を解説します。まずは必要最低限の知識を身につけていきましょう。

型の基本は第3章「型は便利だ楽しいな」を、難しいことや便利なことは第4章「アドバンスド型戦略」を見てください。既存のJavaScript資産やライブラリを使いたい場合は第6章「JavaScriptの資産と@types」や第7章「型定義ファイルを作成する」を参照してください。

また、本書は--strictを有効にした状態を基本として解説します。オプションの詳細については第5章「オプションを知り己のコードを知れば百戦危うからず」を参照してください。

2.1 変数

TypeScriptの変数宣言はおおむねJavaScriptと同じです。違うところはリスト2.1のように変数名のあとに: 型名形式でその変数がどういう型の値の入れ物になるのか指定できるところです。これを型注釈 (type annotations)と呼びます。

リスト2.1: 型注釈つきの変数

// JavaScriptそのものの書き方
// 変数に初期値を与えると初期値の型がそのまま変数の型になる(型推論される)
// 省略しても問題のない型の記述は積極的に省略してしまってよい!
{
  let str = "文字列";
  let num = 1;
  let bool = true;

  let func = () => { };
  let obj = {};

  console.log(str, num, bool, func(), obj);
}

// 型推論に頼らずに型注釈を明示的に書いてもよい
// 特別な理由がない限り、このやり方に長所はない
{
  let str: string = "文字列";
  let num: number = 1;
  let bool: boolean = true;

  let func: Function = () => { };
  // any はなんでも型
  let obj: any = {};

  console.log(str, num, bool, func(), obj);
}

もちろん、変数に対して初期化子を与えることで変数の型をコンパイラに考えさせる(型推論させる)こともできます。TypeScriptはIDEやエディタとの連携が良好なため、型情報はツールチップなどで簡単に確認できます。このため、型推論を多様しても困ることはほぼないため、安心して短く気持ちよく書きましょう。

型がつけられると何が嬉しいかというと、型に反するようなコードを書くとtscコマンドなどでコンパイルしたときにエラーになることです。たとえばリスト2.2のように、整合性がとれていない箇所をコンパイラが見つけてくれます*1

[*1] コンパイルエラーを消すため、今後もサンプルコード中に一見意味のなさそうなexport {}などが表れます

リスト2.2: 型注釈に反することをやってみる

let str: string;
// 文字列は数値と互換性がない!
// error TS2322: Type '1' is not assignable to type 'string'.
str = 1;

let num: number;
// 数値は真偽値と互換性がない!
// error TS2322: Type 'true' is not assignable to type 'number'.
num = true;

let bool: boolean;
// 真偽値は文字列と互換性がない!
//  error TS2322: Type '"str"' is not assignable to type 'boolean'.
bool = "str";

export {}

コンパイルした段階でソースコードの整合性が保たれていない、きな臭い部分があぶり出されるのは嬉しいです。安心安全!

2.2 クラス

TypeScriptではクラスについて、いくつかの拡張が用意されています(リスト2.3)。

リスト2.3: 一般的なクラス要素

class Base {
  // インスタンス変数
  num = 1;

  // 初期値を与えない場合は型の指定が必要
  str: string;

  // プロパティ名に?をつけると省略可能(undefinedである可能性がある)ことを表せる
  regExpOptional?: RegExp;

  constructor(str: string) {
    // strは省略可能じゃないのでコンストラクタで初期値を設定しなければならない
    // 設定し忘れても現在のTypeScriptはエラーにしてくれないので注意が必要…
    this.str = str;
  }

  // メソッドの定義 返り値は省略してもOK
  hello(): string {
    return `Hello, ${this.str}`;
  }

  get regExp() {
    if (!this.regExpOptional) {
      return new RegExp("test");
    }

    return this.regExpOptional;
  }
}

const base = new Base("world");
console.log(base.hello());

export { };

クラスのメンバーを定義する箇所にプロパティを記述していくやり方はTypeScriptの拡張で、ECMAScriptの範囲ではありません。ECMAScriptの場合はコンストラクタ内部でプロパティの設定を行います。コンパイルして出てくるjsコード(リスト2.4)との差を見てみるとわかりやすいです。

リスト2.4: jsにコンパイルしたの出力

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
class Base {
    constructor(str) {
        // インスタンス変数
        this.num = 1;
        // strは省略可能じゃないのでコンストラクタで初期値を設定しなければならない
        // 設定し忘れても現在のTypeScriptはエラーにしてくれないので注意が必要…
        this.str = str;
    }
    // メソッドの定義 返り値は省略してもOK
    hello() {
        return `Hello, ${this.str}`;
    }
    get regExp() {
        if (!this.regExpOptional) {
            return new RegExp("test");
        }
        return this.regExpOptional;
    }
}
const base = new Base("world");
console.log(base.hello());

また、プロパティには省略可能(optional)かを明示する?を指定できます。コンストラクタ内の処理が終わるまでの間に値がセットされないプロパティについては、省略可能なことを明示するようにしましょう。

次にクラスの継承も見て行きましょう(リスト2.5)。superを使い親クラスのメソッドを参照できます。

リスト2.5: もちろん継承もあるよ

class Base {
  greeting(name: string) {
    return `Hi! ${name}`;
  }
}

class Inherit extends Base {
  greeting(name: string) {
    return `${super.greeting(name)}. How are you?`;
  }
}

let obj = new Inherit();
// Hi! TypeScript. How are you? と出力される
console.log(obj.greeting("TypeScript"));

export { }

TypeScript以外のオブジェクト指向言語でもいえることですが、なんでもかんでも継承すればいいや!という考えはよくありません。頑張ってオブジェクト指向に適した設計を行いましょう。

アクセス修飾子

TypeScript固有の機能として、アクセス修飾子があります。プロパティやメソッド、コンストラクタについてprivate、public、protectedといったアクセス修飾子を利用できます(リスト2.6)。何も指定していないとき、デフォルトの可視性はpublicになります。

リスト2.6: アクセス修飾子の例

class Base {
  a = "a";
  public b = "b";
  protected c = "c";
  private d = "d";

  method() {
    // privateなプロパティは利用しているコードが一箇所もないと警告してもらえる
    this.d;
  }
}

class Inherit extends Base {
  method() {
    // 子クラスから public, protected はアクセス可能
    this.a;
    this.b;
    this.c;
    // private はコンパイルエラーになる
    // this.d;
  }
}

const base = new Base();
// public は通常のアクセスが可能
base.a;
base.b;
// protected, private はコンパイルエラーになる
// base.c;
// base.d;

次にコンパイル後のJSファイルを見てみます(リスト2.7)。

リスト2.7: アクセス修飾子はJSコードに影響しない

"use strict";
class Base {
    constructor() {
        this.a = "a";
        this.b = "b";
        this.c = "c";
        this.d = "d";
    }
    method() {
        // privateなプロパティは利用しているコードが一箇所もないと警告してもらえる
        this.d;
    }
}
class Inherit extends Base {
    method() {
        // 子クラスから public, protected はアクセス可能
        this.a;
        this.b;
        this.c;
        // private はコンパイルエラーになる
        // this.d;
    }
}
const base = new Base();
// public は通常のアクセスが可能
base.a;
base.b;
// protected, private はコンパイルエラーになる
// base.c;
// base.d;

アクセス修飾子がきれいさっぱり消えていますね。アクセス修飾子はコンパイル時のみに影響がある機能で、anyのような何でもあり型にキャストすると隠したはずのプロパティにアクセスできてしまいます。外部からの変更を100%防げる!と考えることはできません。筆者はアクセス修飾子を使うだけではなく、privateな要素のprefixに_を使い、ドキュメントコメントに@internalをつけるといった工夫をしています。

引数プロパティ宣言

コンストラクタの引数にアクセス修飾子をあわせて書くと、インスタンス変数としてその値が利用可能になります(リスト2.8)。これを引数プロパティ宣言 (parameter property declaration)と呼びます。引数プロパティ宣言もTypeScript固有の記法です。

リスト2.8: 引数プロパティ宣言!

class BaseA {
  constructor(public str: string) {
  }
}

// BaseA と等価な定義
class BaseB {
  str: string;
  constructor(str: string) {
    this.str = str;
  }
}

export { BaseA, BaseB }

抽象クラス(Abstract Class)

ECMAScriptにはない機能として、抽象クラスが作成できます。抽象クラス単独ではインスタンス化できません。その代わり、抽象クラスを継承したクラスに対して、abstractで指定した要素の実装を強制できます。例を見てみましょう(リスト2.9)。

リスト2.9: 抽象クラス

abstract class Animal {
  abstract name: string;
  abstract get poo(): string;

  abstract speak(): string;
  sleep(): string {
    return "zzzZZZ...";
  }
}
// もちろん、abstract classはそのままではインスタンス化できない
// error TS2511: Cannot create an instance of the abstract class 'Animal'.
// new Animal();

class Cat extends Animal {
  // プロパティの実装を強制される
  name = "Cat";
  poo = "poo...";

  // メソッドの実装を強制される
  speak(): string {
    return "meow";
  }
}

new Cat();

export { }

privateやprotectedに比べると使い勝手がよい機能といえます。便利ですね。

コンパイル後のJavaScriptを見てみると、単なる普通のクラスに変換されていることがわかります(リスト2.10)。

リスト2.10: コンパイルしてしまえばただのクラス

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
class Animal {
    sleep() {
        return "zzzZZZ...";
    }
}
// もちろん、abstract classはそのままではインスタンス化できない
// error TS2511: Cannot create an instance of the abstract class 'Animal'.
// new Animal();
class Cat extends Animal {
    constructor() {
        super(...arguments);
        // プロパティの実装を強制される
        this.name = "Cat";
        this.poo = "poo...";
    }
    // メソッドの実装を強制される
    speak() {
        return "meow";
    }
}
new Cat();

2.3 関数

関数について解説します。JavaScriptでは関数は一級市民ですので、変数に入れたり関数に関数を渡す、いわゆる高階関数もお手の物です。JavaScriptを使いこなすうえで関数のことをしっかり理解すれば、人が書いたコードも読みやすくなります。TypeScriptでも同じことがいえますので、しっかり覚えていきましょう。

普通の関数

関数定義は、いたって普通です(リスト2.11)。型注釈の書き方で通常のJavaScriptと差が出ます。確認していきましょう。

リスト2.11: 色々な関数定義

function hello(word: string): string {
  return `Hello, ${word}`;
}
hello("TypeScript");

// 返り値の型を省略すると返り値の型から推論される
function bye(word: string) {
  return `Bye, ${word}`;
}
bye("TypeScript");

// ? をつけると呼び出しときに引数が省略可能になる
function hey(word?: string) {
  // 省略可能にした時はundefinedの時の考慮が必要!
  return `Hey, ${word || "TypeScript"}`;
}
hey();

// デフォルト値の指定で仮引数の型を省略したりもできる
function ahoy(word = "TypeScript") {
  return `Ahoy! ${word}`;
}
ahoy();

export { }

可変長引数の場合は仮引数の最終的な型を書きます(リスト2.12)。つまり、配列の形になりますね。

リスト2.12: 可変長引数の例

function hello(...args: string[]) {
  return `Hello, ${args.join(" & ")}`;
}
// Hello, TS & JS と表示される
console.log(hello("TS", "JS"));

export { }

なお、省略可能引数の後に省略不可な引数を配置したり、可変長引数を最後以外に配置したりすることはできません(リスト2.13)。

リスト2.13: こういうパターンはNG

// 省略可能な引数の後に省略不可な引数がきてはいけない
// error TS1016: A required parameter cannot follow an optional parameter.
function funcA(arg1?: string, arg2: string) {
  return `Hello, ${arg1}, ${arg2}`;
}

// 可変長引数は必ず最後じゃないといけない
// error TS1014: A rest parameter must be last in a parameter list.
function funcB(...args: string[], rest: string) {
  return `Hello, ${args.join(", ")} and ${rest}`;
}

export { }

ここまで見てきた省略可能な引数やデフォルト値付き引数、可変長引数はクラスのコンストラクタやメソッドを記述するときも同様に利用できます。

アロー関数

アロー関数 (Arrow Functions)を見ていきましょう(リスト2.14)。通常の関数とアロー関数の違いについてはECMAScriptの仕様の範囲ですので省略します。

リスト2.14: アロー関数 短くてかっこいい

// NOTE ここのcallbackの型注釈の意味は別の章で解説します
// 引数を1つ取って返り値無し の関数を表します
function asyncModoki(callback: (value: string) => void) {
  callback("TypeScript");
}

// アロー関数をコールバック関数として渡す 渡す関数の型は型推論される!
asyncModoki(value => console.log(`Hello, ${value}`));

// アロー関数に明示的に型付をする場合
asyncModoki((value: string): void => console.log(`Hello, ${value}`));

export { }

アロー関数も普通の関数同様、型注釈の与え方以外ECMAScriptの仕様との差分はありません。書きやすくてよいですね。

アロー関数では親スコープのthisをそのまま受け継ぎます。この仕組みのおかげでクラスのメソッドなどでコールバック関数を使うときに無用な混乱をおこさずに済みます。特別な理由が思いつかない限りアロー関数を使っておけばよいでしょう。

Async(非同期)関数

俗にasync/awaitと呼ばれる仕様です。async/awaitの振る舞いについてはECMAScript仕様の範囲ですので概要だけ説明します。ES2015で標準仕様に入った非同期処理APIのPromiseがあります。これらに簡易な構文を与えたものがAsync関数です(リスト2.15*2

[*2] ちなみにasync/awaitのdownpileもTypeScript 2.1.1からサポートされています

リスト2.15: async/await 便利!

function returnByPromise(word: string) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(word);
    }, 100);
  });
}

// async functionの返り値の型は必ずPromiseになる
async function helloAsync(): Promise<string> {
  // この関数は実行すると A, TypeScript, B が順番に表示される

  console.log("A");
  // Promiseな値をawaitすると中身が取り出せる(ように見える)
  const word = await returnByPromise("TypeScript");
  console.log(word);
  console.log("B");

  return `Hello, ${word}`;
}

// awaitが使えるのは必ずasync functionの中
(async () => {
  const hello = await helloAsync();
  console.log(hello);
})();

// 普通にPromiseとして扱うこともできる
helloAsync().then(hello => console.log(hello));

export { }

Async関数の返り値の型は常にPromiseになります。

ちなみに、generatorの返り値の型は常にIterableIterator、async generatorの場合は常にAsyncIterableIteratorとする必要があります。

2.4 モジュールのあれこれ

プログラムの規模が大きくなればなるほど、機能ごとに分割して統治し、見通しをよくする必要があります。そのための武器として、ECMAScript 2015から言語にモジュールの仕様が追加されました。1つのJSファイルを1つのモジュールと捉えます。つまり、別ファイルになれば別モジュールと考え、モジュールから値をexportしたりimportしたりして大きなプログラムを分割し統治します。

歴史的経緯により、TypeScriptではモジュールの他にnamespaceという機能があります。モジュールの考え方がまだ発達していなかった時代に、関数を使ってモジュールのようなものを作っていた名残ですね。

仕様としてモジュールが策定され、ブラウザでの実装も進んでいる今、namespaceを使ってプログラムを分割・構成するのは悪手です*3。これから新規にプロジェクトを作成する場合は実行環境がNode.jsであれ、ブラウザであれ、モジュールを使って構成するべきでしょう。

[*3] なお、TypeScriptコンパイラ本体はまだnamespaceを使っている模様

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

今は使われていない、昔の用語の使い方について参考文献としてメモしておきます。ここに書いてあることは知らないほうがよい知識かもしれません。

歴史的経緯により、TypeScriptはモジュールのことを外部モジュール(External Modules)と呼んでいました。また、namespaceのことを内部モジュール(Internal Modules)と呼んでいました。内部モジュールとは、関数を使って1つの名前空間を作り出すテクニックのことで、ECMAScriptの仕様に含まれるものではありません。

ECMAScript 2015で本格的に"モジュール"の概念が定義されたため、TypeScriptでは今後はモジュールといえば外部モジュールを指し、内部モジュールのことはnamespaceと呼ぶように改めました。これにあわせて、内部モジュールの記法も古くはmoduleを使っていたのをnamespaceに変更されました。

本書でも、単にモジュールと書く場合は外部モジュールのことを指し、namespaceと書いた時は内部モジュールのことを指しています。

モジュール

モジュールは前述のとおり、1ファイル=1モジュールとしてプロジェクトを構成していく方式です。import * as foo from "./foo";のように書くと、記述したファイルから./foo.ts*4を参照できます。ここでは、./fooがひとつのモジュールとして扱われます。

[*4] Node.js上の仕様(TypeScriptではない)について細かくいうと、require("./foo")すると最初に./foo.js が探され、次に./foo.json、./foo.nodeと検索します

TypeScriptではCommonJS、AMD、System(SystemJS)、UMD、ECMAScript 2015によるモジュールの利用に対応しています。いずれの形式で出力するかについては--module commonjsオプションで指定できます。

本書ではNode.jsでもBrowserifyやwebpackで広く利用しやすいCommonJS形式についてのみ言及します。rollup.jsなどの普及により、es2015形式のまま出力し別途bundlerで処理する場合もあるかもしれません。

さて、実際にコードを見てみましょう。foo.ts(リスト2.16)、bar.ts(リスト2.17)、buzz.ts(リスト2.18)というファイルがあるとき、それぞれがモジュールになるので3モジュールある、という考え方になります。

リスト2.16: foo.ts

// defaultをbarという名前に hello関数をそのままの名前でimport
import bar, { hello } from "./bar";
// モジュール全体をbar2に束縛
import * as bar2 from "./bar";
// ECMAScript 2015形式のモジュールでもCommonJS形式でimportできる
import bar3 = require("./bar");

// Hello, TypeScript! と表示される
console.log(hello());
// Hi!, default と表示される
console.log(bar());
// 上に同じく Hello, TypeScript! と Hi!, default
console.log(bar2.hello());
console.log(bar2.default());
// 上に同じく Hello, TypeScript! と Hi!, default
console.log(bar3.hello());
console.log(bar3.default());

// export = xxx 形式の場合モジュール全体をbuzzに束縛
import * as buzz from "./buzz";
// CommonJS形式のモジュールに対して一番素直で真っ当な書き方 in TypeScript
import buzz2 = require("./buzz");
// 両方 Good bye, TypeScript! と表示される
console.log(buzz());
console.log(buzz2());

リスト2.17: bar.ts

export function hello(word = "TypeScript") {
  return `Hello, ${word}`;
}

export default function(word = "default") {
  return `Hi!, ${word}`;
}

リスト2.18: buzz.ts

function bye(word = "TypeScript") {
  return `Good bye, ${word}`;
}
// foo.ts でECMAScript 2015形式でimportする際、
// 次のエラーが出るのを抑制するためのハック
// error TS2497: Module '"略/buzz"' resolves to a non-module entity
//   and cannot be imported using this construct.
namespace bye { }

// CommonJS向け ECMAScript 2015では× 今後は使わなくてよし!
export = bye;

各モジュールのトップレベルでexportしたものが別のファイルからimportして利用できているのがわかります。コンパイルして結果を確かめてみましょう。Node.jsに慣れている人なら、見覚えのある形式のコードが出力されていることが分かるでしょう。

$ tsc --module commonjs --target es2015 foo.ts
$ cat foo.js
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
// defaultをbarという名前に hello関数をそのままの名前でimport
const bar_1 = require("./bar");
// モジュール全体をbar2に束縛
const bar2 = require("./bar");
// ECMAScript 2015形式のモジュールでもCommonJS形式でimportできる
const bar3 = require("./bar");
// Hello, TypeScript! と表示される
console.log(bar_1.hello());
// Hi!, default と表示される
console.log(bar_1.default());
// 上に同じく Hello, TypeScript! と Hi!, default
console.log(bar2.hello());
console.log(bar2.default());
// 上に同じく Hello, TypeScript! と Hi!, default
console.log(bar3.hello());
console.log(bar3.default());
// export = xxx 形式の場合モジュール全体をbuzzに束縛
const buzz = require("./buzz");
// CommonJS形式のモジュールに対して一番素直で真っ当な書き方 in TypeScript
const buzz2 = require("./buzz");
// 両方 Good bye, TypeScript! と表示される
console.log(buzz());
console.log(buzz2());

$ cat bar.js
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
function hello(word = "TypeScript") {
    return `Hello, ${word}`;
}
exports.hello = hello;
function default_1(word = "default") {
    return `Hi!, ${word}`;
}
exports.default = default_1;

$ cat buzz.js
"use strict";
function bye(word = "TypeScript") {
    return `Good bye, ${word}`;
}
module.exports = bye;

動的インポート(Dynamic Import)

TypeScript 2.4系からサポートされたECMAScriptの仕様に動的インポートがあります。ECMAScriptの仕様上、モジュールのimport文は参照するモジュールを動的に変える余地がありませんでした。これは、プログラムを実行しなくてもパースした時点で必要なファイルの全リストを作れるという利点があります。

この仕様は90%のユースケースを満足させるかもしれませんが、動的に必要なモジュールを決定できることにより得られる柔軟性もあります。そのために、動的インポートの仕様が策定されています。コード例を見てみましょう(リスト2.19リスト2.20)。

リスト2.19: 実行時に動的にインポートするモジュールを決定する

async function main() {
  // 動的にモジュールをimportできる Promiseが返ってくる
  // 即値(文字列リテラル)でモジュール名を指定するとちゃんと型がついてる!
  const sub = await import("./sub");
  console.log(sub.hello());
}

function mainAnother() {
  // こういうのも当然OK!
  import("./sub").then(sub => {
    console.log(sub.hello());
  });
}

main();

リスト2.20: なんの変哲もないimportされる側

export function hello(word = "world") {
  return `Hello, ${word}`;
}

わかりやすいですね。TypeScript上での特徴として、importに渡す文字列が固定の場合、これは実行せずに解析できるため得られたモジュールにはしっかりと型がついています。動的に組み立てた文字列を渡した場合、なんでもありのanyになってしまうため、自分で独自に型注釈を与えたほうが安全に使えます。

なお、動的インポートを無変換でJSに出力したい場合、--module esnextが必要で、--module es2015ではエラーになるので注意しましょう。

namespace

現実的にコードを書く時にはnamespaceを使わないほうがよいです。ですので、できればnamespaceについては説明したくないのですが、そうはいかない理由があります。それが、型定義ファイルの存在です。

型定義ファイルの中ではインタフェースや関数などをきれいに取りまとめるためにnamespaceの仕組みを活用する場面がでてきます。そのため、TypeScriptの習熟度を高めるうえでnamespaceは避けては通れない要素です。

ECMAScript 5以前の時代にはモジュールはおろかブロックスコープという概念もありませんでした。これを補うため、関数を定義するとスコープが作れることを応用し、モジュールっぽい構造を自力で作成していました。その工夫に対して、独自の構文を割り当てたものがTypeScriptのnamespaceです。

まずは簡単な例を見てみましょう(リスト2.21)。

リスト2.21: namespaceを使ったコード

namespace a {
  // export してないものは外部からは見えない
  class Sample {
    hello(word = "TypeScript") {
      return `Hello, ${word}`;
    }
  }

  export interface Hello {
    hello(word?: string): string;
  }
  export let obj: Hello = new Sample();
}
namespace a {
  export function bye(word = "JavaScript") {
    return `Bye, ${word}`;
  }

  // 定義を分けてしまうと同名のモジュールでもexportされていないものは見えない
  // error TS2304: Cannot find name 'Sample'.
  // let tmp = new Sample();
}

// ネストしたnamespace
namespace b {
  export namespace c {
    export function hello() {
      return a.obj.hello();
    }
  }
}
// ネストしたnamespaceの短縮表記も存在する
namespace d.e {
  export function hello() {
    return a.obj.hello();
  }
}

// Hello, TypeScript と表示される
console.log(b.c.hello());
// Hello, TypeScript と表示される
console.log(d.e.hello());

なかなかシンプルです。namespaceの内側で定義した要素はクラスであれ、関数であれ、なんであってもexportをつけなければ外側から見えないようになります。

これをコンパイルした結果を確認してみます(リスト2.22)。

リスト2.22: コンパイルすると関数を使った構文に展開される

"use strict";
var a;
(function (a) {
    // export してないものは外部からは見えない
    class Sample {
        hello(word = "TypeScript") {
            return `Hello, ${word}`;
        }
    }
    a.obj = new Sample();
})(a || (a = {}));
(function (a) {
    function bye(word = "JavaScript") {
        return `Bye, ${word}`;
    }
    a.bye = bye;
    // 定義を分けてしまうと同名のモジュールでもexportされていないものは見えない
    // error TS2304: Cannot find name 'Sample'.
    // let tmp = new Sample();
})(a || (a = {}));
// ネストしたnamespace
var b;
(function (b) {
    let c;
    (function (c) {
        function hello() {
            return a.obj.hello();
        }
        c.hello = hello;
    })(c = b.c || (b.c = {}));
})(b || (b = {}));
// ネストしたnamespaceの短縮表記も存在する
var d;
(function (d) {
    var e;
    (function (e) {
        function hello() {
            return a.obj.hello();
        }
        e.hello = hello;
    })(e = d.e || (d.e = {}));
})(d || (d = {}));
// Hello, TypeScript と表示される
console.log(b.c.hello());
// Hello, TypeScript と表示される
console.log(d.e.hello());

関数を使って名前空間を擬似的に作っている様子が確認できます。モジュールもブロックスコープもなかった時代は辛かったですね。

長い名前を使うのが嫌なときはリスト2.23のように、import句を使うこともできます。先に説明したモジュールではこれとは異なるimport句の使い方が出てきましたが、区別しましょう。

リスト2.23: import句で別名を作る

namespace a {
  export class Sample { }
}

namespace b {
  // 他のモジュールも参照できる
  let objA: a.Sample;
  objA = new a.Sample();

  // めんどくさいなら import句 を使えばいい
  import Sample = a.Sample;
  let objB: Sample;
  objB = new Sample();

  // 別に違う名前をつけてもいい(けど混乱しちゃうかも?
  import Test = a.Sample;
  let objC: Test;
  objC = new Test();

  // 別に名前が違っても互換性が失われるわけではないのだ
  objA = new Test();
}

2.5 enumとconst enum

基本となる知識かと問われるとちょっと微妙な気持ちになるenumです。ECMAScriptの範囲にある仕様ではない、TypeScript独自の仕様なのでenumはなるべく利用せず、const enumだけで運用していきたいものです。

enumを使うと、自分で選んだ名前と値の集合を作ることができます。const enumはそこから一歩進んで、コンパイル時にすべての値をインライン展開し定数値に置き換えます。まずはtsコード(リスト2.24)と生成されたjsコード(リスト2.25)を確認します。

リスト2.24: enumとconst enumの例

enum Suit {
  Heart,
  Diamond,
  Club,
  Spade,
}
// 0, 'Heart' と表示される
console.log(Suit.Heart, Suit[Suit.Heart]);

const enum Permission {
  Execute = 1,
  Read = 2,
  Write = 4,
  All = Execute | Read | Write,
}
// 7 と表示される
console.log(Permission.All);

enum Tree {
  Node = "node",
  Leaf = "leaf",
}
// node と表示される
console.log(Tree.Node);

export { Suit, Permission, Tree }

リスト2.25: 生成されたjs constはコンパイルすると消える

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var Suit;
(function (Suit) {
    Suit[Suit["Heart"] = 0] = "Heart";
    Suit[Suit["Diamond"] = 1] = "Diamond";
    Suit[Suit["Club"] = 2] = "Club";
    Suit[Suit["Spade"] = 3] = "Spade";
})(Suit || (Suit = {}));
exports.Suit = Suit;
// 0, 'Heart' と表示される
console.log(Suit.Heart, Suit[Suit.Heart]);
// 7 と表示される
console.log(7 /* All */);
var Tree;
(function (Tree) {
    Tree["Node"] = "node";
    Tree["Leaf"] = "leaf";
})(Tree || (Tree = {}));
exports.Tree = Tree;
// node と表示される
console.log(Tree.Node);

enumは変数に展開され、const enumは実行コードから消えています。enumの値に指定できるのはnumberかstringで、numberの場合は実行時に数値からプロパティの名前を逆引きできるようになっています。また、値はある程度、計算結果を利用できます。

const enumについて、tscに--preserveConstEnumsオプションを渡してやるとenum相当のコードが生成されるようになります。デバッグ時にはこのオプションを用いたほうが処理を追いかけやすい場合もあるでしょう。