第2章 TypeScriptの基本

第1章「戦闘準備だ!TypeScript!」で述べたとおり、本書ではECMAScript 2015の文法・仕様についてすべてを解説することはしません。ECMAScript 2015の知識はどんどん広まってきていますし、今後は基本的なJavaScriptの知識になっていくでしょう。ECMAScriptの知識は、TypeScript固有の知識ではないですからね。

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

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

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

2.1 変数

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

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

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

let str: string;
let num: number;
let bool: boolean;

let func: Function;
let obj: any; // なんでも型

str = "文字列";
num = 1;
bool = true;
func = () => { };
obj = {};

export { }

型注釈の何が嬉しいかというと、型に反するようなコードを書くとtscコマンドを使ってコンパイルしたときにコンパイルエラーになるのです。たとえばリスト2.2のように、整合性がとれていない箇所がTypeScriptによって明らかにされます。安心安全!

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

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

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

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

安心安全なのはよいですが、わかりきったことを書くのは省きたいと思うのはエンジニアの性分でしょう。そんなあなたのために、TypeScriptは型推論の機能を備えています。リスト2.3のように、型注釈を書かずに変数定義と初期化を同時に行えます。

リスト2.3: 初期化付き変数 = 最強

let str = "string";
let num = 1;
let bool = true;

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

export { str, num, bool, func, obj }

これで手で型注釈を与えずに済みます。しかも、書き方がJavaScriptと全く同じになりました。楽に書ける上に実行前にコンパイルの段階で不審な臭いのするコードを発見できるようになる、第一歩です。

2.2 クラス

普通のクラス

ECMAScript 2015より導入されたクラス構文についても各所に型注釈可能な構文が追加されています(リスト2.4)。

リスト2.4: さまざまなクラス要素

class Base {
  // インスタンス変数
  numA: number;
  strA = "string";
  public numB: number;
  private numC: number;
  protected numD: number;
  regexpA?: RegExp;

  // クラス変数
  static numA: number;
  public static numB: number;
  private static numC: number;
  protected static numD: number;
  static regexpA?: RegExp;

  // コンストラクタ
  constructor(boolA: boolean,
    public boolB: boolean,
    private boolC: boolean,
    protected boolD: boolean) {
    // エラー消し 一回も使われない可能性があると怒られる
    console.log(boolA, this.numC, this.boolC, Base.numC);
  }

  // メソッド
  hello(word: string): string {
    return "Hello, " + word;
  }

  // get, setアクセサ
  // コンパイル時に --target es5 以上が必要です
  /** @internal **/
  private _date: Date;
  get dateA(): Date {
    return this._date;
  }
  set dateA(value: Date) {
    this._date = value;
  }

  optional() {
    // 省略可能なプロパティは値の存在チェックが必要です
    if (this.regexpA != null) {
      this.regexpA.test("Hi!");
    }
    if (Base.regexpA != null) {
      Base.regexpA.test("Hi!");
    }
  }
}

let obj = new Base(true, false, true, false);
obj.numA;
obj.strA;
obj.numB;
// obj.numC; // private   なメンバにはアクセスできない
// obj.numD; // protected なメンバにもアクセスできない
obj.boolB;
// obj.boolC; // private   なメンバにはアクセスできない
// obj.boolD; // protected なメンバにもアクセスできない
obj.hello("TypeScript");
obj.dateA = new Date();
obj.dateA;

export { }

上から順に見て行きましょう。

まずはクラス変数、インスタンス変数です。クラスそのものやインスタンスに紐づく変数です。JavaScriptっぽくいうとプロパティですね。

アクセス修飾子として、private、public、protectedなどの可視性を制御するアクセス修飾子を利用できます。何も指定していないとき、デフォルトの可視性はpublicになります。

コンパイル後のJSファイルを見るとわかりますがanyにキャストするとそれらの要素にアクセスできてしまうので、アクセス修飾子をつけたから外部からの変更を100%防げる!と考えるのは禁物です。そのため筆者はアクセス修飾子を使うだけではなく、privateな要素のprefixに_を使い、ドキュメントコメントに@internalをつけるといった工夫をしています。

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

次はコンストラクタです。コンストラクタ自体にも前述のprivate、protectedなどのアクセス修飾子を利用できます。

引数にアクセス修飾子をあわせて書くと、インスタンス変数としてその値が利用可能になります。これを引数プロパティ宣言 (parameter property declaration)と呼びます。引数プロパティ宣言はTypeScript固有の記法です。そもそも、JavaScriptにはアクセス修飾子がありませんからね。リスト2.5のようなコードを書くとリスト2.6のようなJavaScriptが出てきます。

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

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

let obj = new Sample("TypeScript");
// TypeScript と表示される
console.log(obj.str);

export { }

リスト2.6: コンパイルするとこんなの

"use strict";
class Sample {
    constructor(str) {
        this.str = str;
    }
}
let obj = new Sample("TypeScript");
// TypeScript と表示される
console.log(obj.str);

リスト2.4の解説に戻ります。次はメソッドです。これも特に特筆すべき要素はありませんね。普通です。

最後にget、setアクセサです。これを含んだコードをコンパイルするときは、--target es5以上を指定します。get、setアクセサを使うと、getterしか定義していない場合でもプログラム上は値の代入処理が記述できてしまうので、"use strict"を併用して実行時にエラーを検出するようにしましょう。

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

リスト2.7: 普通に継承もあるよ

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

抽象クラス(Abstract Class)

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

リスト2.8: 抽象クラス

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.9)。

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

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

2.3 関数

普通の関数

いたって普通です(リスト2.10)。型注釈の与え方や、引数を省略可能にする方法だけがJavaScriptと違いますね。

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

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.11)

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

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

export { }

なお、省略可能引数の後に省略不可な引数を配置したり、可変長引数を最後以外に配置したりするのはNGです(リスト2.12)。

リスト2.12: こういうパターンは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 { }

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

アロー関数

ECMAScript 2015で導入されたアロー関数 (Arrow Functions)を見ていきましょう(リスト2.13)。通常の関数とアロー関数の違いについてはECMAScript 2015の範囲であるため、本書では解説しません。

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

// 次の2つは(thisが絡まない限り)等価!
let funcA = () => true;
let funcB = function() {
  return true;
};
funcA();
funcB();

// NOTE ここのcallbackの型注釈の意味は別の章で解説します
// 引数を1つ取って返り値無し の関数を表します
function asyncModoki(callback: (value: string) => void) {
  callback("TypeScript");
}
// ES5時代の書き方
asyncModoki(function(value: string) {
  console.log(`Hello, ${value}`);
});
// アロー関数だとさらに楽
asyncModoki(value => console.log(`Hello, ${value}`));
// アロー関数に型付をする場合
asyncModoki((value: string): void => console.log(`Hello, ${value}`));

export { }

アロー関数も普通の関数同様、型注釈の与え方以外ECMAScript 2015との差分は見当たりません。短くてかっこいいですね。

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

2.4 モジュールのあれこれ

プログラムの規模が大きくなればなるほど、機能ごとに分割して統治し、見通しをよくする必要があります。そのための武器として、ECMAScript 2015にはモジュールがあります。1つのJSファイルを1つのモジュールと捉えます。Node.jsで使われているCommonJS形式のモジュールと考え方は一緒です。つまり、別ファイルになれば別モジュールと考え、モジュールから値をexportしたりimportしたりして大きなプログラムを分割し統治します。

歴史的経緯により、TypeScriptでは先に説明した1つのJavaScriptファイルを1つのモジュールと捉えた形式のことを外部モジュール(External Modules)と呼び、関数を使って1つの名前空間を作り出す形式を内部モジュール(Internal Modules)と呼んでいました。しかし、ECMAScript 2015で本格的に"モジュール"の概念が定義されたため、TypeScriptでは今後はモジュールといえば外部モジュールを指し、内部モジュールのことはnamespaceと呼ぶようになりました。これにあわせて、内部モジュールの記法も旧来のmoduleからnamespaceに変更されました。未だにmoduleを使うこともできますが、今後はnamespaceを使ったほうがよいでしょう。

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

仕様としてモジュールが策定され、WHATWGでブラウザでのモジュールの動作について議論が進んでいる現状、namespaceのみを使ってプログラムを分割・構成すると将来的にはきっと負債になるでしょう。これから新規にプロジェクトを作成する場合は実行する環境がNode.jsであれ、ブラウザであれ、モジュールを使って構成するべきでしょう。

モジュール

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

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

本書ではNode.jsでもBrowserifyやwebpackで広く利用しやすいCommonJS形式についてのみ言及します。対応形式の中ではAMDやSystemJSについては癖が強く、tscに与えることができるオプションの数も多いため興味がある人は自分で調べてみてください。筆者は両形式はあまり筋がよいとは今のところ思っていませんけれど。

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

リスト2.14: 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.15: bar.ts

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

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

リスト2.16: 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 es6 foo.ts
$ cat foo.js
"use strict";
// 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";
function hello(word = "TypeScript") {
    return `Hello, ${word}`;
}
exports.hello = hello;
function default_1(word = "default") {
    return `Hi!, ${word}`;
}
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = default_1;
$ cat buzz.js
"use strict";
function bye(word = "TypeScript") {
    return `Good bye, ${word}`;
}
module.exports = bye;

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

namespace

現実的にコードを書く時にはnamespaceを使わないほうがよいのです。ですので、できればnamespaceについては説明したくないのですが、そうはいかない理由があります。それが、型定義ファイルの存在です。型定義ファイルの中ではインタフェースや関数などをきれいに取りまとめるためにnamespaceの仕組みを活用する場面がでてきます。そのため、TypeScriptの習熟度を高めるうえでnamespaceは避けては通れないのです。

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

リスト2.17: 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 b {
  export namespace c {
    export function hello() {
      return a.obj.hello();
    }
  }
}
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.18)。

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

var a;
(function (a) {
    // export してないものは外部からは見えない
    class Sample {
        hello(word = "TypeScript") {
            return `Hello, ${word}`;
        }
    }
    a.obj = new Sample();
})(a || (a = {}));
var a;
(function (a) {
    function bye(word = "JavaScript") {
        return `Bye, ${word}`;
    }
    a.bye = bye;
})(a || (a = {}));
var b;
(function (b) {
    var c;
    (function (c) {
        function hello() {
            return a.obj.hello();
        }
        c.hello = hello;
    })(c = b.c || (b.c = {}));
})(b || (b = {}));
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());

関数を使って名前空間を擬似的に作っています。モジュールもletやconstのようなブロックスコープもなかった頃の名残ですね。

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

リスト2.19: 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();
}