January 2017

Volume 32 Number 1

働くプログラマ - MEAN あれこれ: TypeScript によるスクリプトの入力

Ted Neward | January 2017

Ted Neward「MEANers」の皆さん、お帰りなさい。

MEAN について取り上げるこのシリーズを続けて 1 年半になります。この間、トピック リストに興味深い変化がありました。それは、シリーズ名の「A」の部分、 AngularJS バージョン 2 の正式リリースで行われた大きな転換です。これにより、フレームワークの奥深くに (重大な) 変化が生じました。最も注目すべき変化の 1 つは、AngularJS 2 アプリケーションをビルドするための「最適な AngularJS 言語」として TypeScript が導入されたことです。TypeScript は、バージョン 1.x 系列のプレーンで面白みのない ECMAScript とは対照的な言語です。

当然読者の多くは、レドモンドのコード開発チームが発表した最新言語の TypeScript にはなじみがあると思います。今回は、あまりなじみのない方のために、この TypeScript を取り上げます。TypeScript は、このシリーズで以前取り上げた ECMAScript 2015 構文によく似ていますが、型情報というちょっとした違いがあります。この言語について本格的に知りたい方は、MSDN マガジン 2015 年 1 月号の Peter Vogel のコラム「TypeScript について」 (msdn.com/magazine/dn890374) をご覧ください。

ただし、AngularJS 2 で使用するのは TypeScript の機能の特定のサブセットです。また、TypeScript 構文が使いやすいと納得していない読者もいます。TypeScript には、Vogel がコラムを執筆した時点から、少し変更が加えられています (2015 年 9 月下旬にバージョン 2 が公開されました)。そのため、AngularJS 2 を取り上げる前に、同じ情報を共有できるように、TypeScript 言語について簡単に触れておきます。

では、TypeScript について見ていきましょう。

「スクリプト (Script)」への「型 (Type)」の追加

TypeScript は概念的にはわかりやすい言語です。 従来の ECMAScript 構文を採用し、型の注釈というかたちで型情報 (省略可能) をいくつか追加しています。これは、F# などの関数言語で型宣言を提供する方法に似ています。TypeScript コンパイラは、ソースからソースを生成するため、技術的には「トランスパイラ」と呼ばれ、プロセスの外部で ECMAScript コードを生成します。このコンパイラでは、すべての型情報が尊重され、型情報に従うことが検証されます。ただし、結果は、以前からブラウザーでよく使われている JavaScript の動的型指定と同じです。つまり、TypeScript の目標は、基になる JavaScript ブラウザー プラットフォームを変更したり、別のプラットフォーム上に新たなプラットフォームを構築するといった高コストな処理を行う必要なく、C# などのタイプセーフな言語のメリット (静的検証による明らかなコード エラーの削減) をすべて利用することです。TypeScript の核となる概念の 1 つは、「正規の ECMAScript プログラムはすべて正規の TypeScript プログラムでもある」 ことから、TypeScript を導入する場合は段階的な作業が可能になります。新しい構文 (CoffeeScript、Fantom、ClojureScript など、まったく新しいトランスパイル済みの言語に必要となる可能性がある構文など) を全面的に導入するのではなく、少しずつ手順を進め、チームが快適と感じられる範囲でのみ新機能を取り入れます。

このことを念頭に置きながら、TypeScript の説明を始めることにします。今回は、AngularJS 2 で最も使用する機能や顕著な機能を取り上げます。残りの機能は今後のさらなる調査に託そうと考えています。

TypeScript のインストール

最初に注目するのは、Node.js ベースの大半のパッケージと同様、TypeScript が npm パッケージであることです。したがって、通常の「npm install」コマンドを使用して TypeScript をインストールします。

npm install –g typescript

TypeScript はグローバル コマンド ("tsc") をインストールするため、TypeScript のインストール時には "-g" (「グローバル」フラグ) を使用することが重要です。"tsc" を実行後、コマンドライン ツールが完全にインストールされていることを確認しておきます。

$ tsc --version
Version 2.0.3

TypeScript をインストールしたら、次は TypeScript コードを作成します。

モジュール

まずは、TypeScript のモジュールについて説明します。

ここでは、コンポーネントを含む person.ts というファイルを作成します (「コンポーネント」は、TypeScript では重要になりませんが、AngularJS 2 では重要になります)。 最初に、別のファイルから呼び出す簡単な関数を作成します。まずは以下の関数を作成します。

function sayHello(message: string) {
  console.log("Person component says", message);
}

パラメーターに型の注釈があるのがわかります。ここでは 1つのパラメーターに文字列を指定しなければなりません。これは、TypeScript の基盤となります。これで、パラメーターに文字列しか渡せなくなります。この関数はそれ自体で単純なコンポーネントになります。ただし、複雑でも単純でも、コンポーネントとして役に立つ必要があります。

早速使ってみましょう。TypeScript アプリケーションでは、次のように import ステートメントを使ってコンポーネントを使用します。

import { sayHello } from './person';
sayHello("Fred");

この "import" ステートメントでは、"person" モジュールの "sayHello" という要素を使用することを宣言しています (import 構文には他にも形式がありますが、それについては後ほど説明します)。 ここで tsc コンパイラを使って、前述の person.ts とこのコードの app.ts の 2 つのファイルを実行します。

tsc person.ts app.ts

残念ながら、TypeScript は person.ts がモジュールでないことを警告します。

TypeScript 環境でのモジュールとは、密接にグループ化されたコードのセットを収納する「ボックス」を提供する言語コンストラクトです。前述のとおり、person.ts はこれまでの JavaScript シナリオではモジュールとして簡単に使用できます。ファイルで関数を定義し、そのファイルを参照するだけで、その関数がグローバル スコープに配置されます。ですが、TypeScript ではもっと明示的な構文が必要になります。以下のように、export キーワードを使用して、外部からアクセスするコンポーネント領域を宣言する必要があります。

export function sayHello(message: string) {
  console.log("Person component says", message);
}

TypeScript では、最上位レベルに import ステートメントまたは export ステートメントを含むファイルがモジュールと見なされます。そのため、この関数をエクスポートすることを宣言するだけで、person.ts のすべてがモジュールとして暗黙のうちに定義されます。

この変更を行ったら、TypeScript は問題なく person.js と app.js という新しい 2 つのファイルを作成します。これらのファイルはファイル システムに保存され、使用されるのを待機します。

クラスの追加

TypeScript では、その基盤となる ECMAScript 2015 言語のように、クラスの主要概念を理解します。そこで、Person をクラスとして定義して使用してみます (図 1 参照)。

図 1 シンプルな Person クラス

export class Person {
  firstName: string;
  lastName: string;
  constructor(fn: string, ln: string) {
    this.firstName = fn;
    this.lastName = ln;
  }
  greet() : string {
    return this.fullName + " says hello!";
  }
  get fullName() : string {
    return this.firstName + " " + this.lastName;
  }
}

このコードは、TypeScript の使用経験がなくても比較的簡単に理解できます。export キーワードは、ここでもこのクラスがモジュール外から使用されることを示しています。firstName フィールドと lastName フィールドは、TypeScript の注釈を使用してコンパイラに「文字列の使用」を強制し、greet メソッドは呼び出し側に文字列を返し、fullName メソッドは firstName フィールドと lastName フィールドで構成される、読み取り専用の合成プロパティ アクセサーとして宣言します。app.ts ファイルで Person 型を使用するのも簡単です。person.ts ファイルから Person 型をインポートし、new キーワードを使用してそのインスタンスを作成するだけです。

import { Person } from './Person';
let ted = new Person("Ted", "Neward");
console.log(ted.greet());

よく見ると、import 行が変わっているのがわかります。今回は sayHello ではなく、Person 型を指定しています。import ステートメントのかっこ間に Person でエクスポートされるシンボルを単純にすべて含めることは可能ですが、すぐに面倒に感じることになります。そこで、TypeScript では、import ステートメントでワイルドカードを指定できるようにしています。ただし、モジュールの中でエクスポートするすべての名前をグローバル名前空間に設定するわけではないため、グローバル名前空間に設定した名前を表現する名前を指定する必要があります。ワイルドカードを使用すると、アプリケーション コードは以下のように少し変わります。

import * as PerMod from './Person';
let ted = new PerMod.Person("Ted", "Neward");
console.log(ted.greet());

PerMod はセンスのない名前なので、これは明らかに運用レベルのコードではありません。

TypeScript でのインターフェイス指定

もちろん、コンポーネントベースの開発 (繰り返しますが、これは AngularJS 2 で重要です) の一般的な目標の 1 つは、コンポーネントのユーザーがコンポーネントを使用する方法と、コンポーネントで有用性を実現する方法を明確に分けることです。つまり、「インターフェイスと実装」を区別することが重要です。TypeScript は、概念がよく似た C# に倣って、インターフェイスを宣言する機能を提供します。これにより、C# と同様、実装で実現する動作を規定します。

そのため、Person コンポーネントで実装に制約を設ける必要なく異なる Person を区別する場合は、Person をインターフェイスとして定義して、複数の異なる実装を用意します。その結果、Person コンポーネントは、それぞれの Person の細かい違いを気にすることなく、簡単に構築できるコンストラクター関数になります (図 2 参照)。

図 2 Person の作成

export function createPerson(
  firstName: string, lastName: string, occupation: string) : Person {
  if (occupation == "Programmer")
    return new Programmer(firstName, lastName);
  else if (occupation == "Manager")
    return new Manager(firstName, lastName);
  else
    return new NormalPerson(firstName, lastName);
}
export interface Person {
  firstName: string;
  lastName: string;
  greet() : string;
  fullName: string;
}

implements キーワードを使用すれば、Person を実装するクラスを簡単に作成できます (図 3 参照)。

図 3 Person の実装

class NormalPerson implements Person {
  firstName: string;
  lastName: string;
  constructor(fn: string, ln: string) {
    this.firstName = fn;
    this.lastName = ln;
  }
  greet() : string {
    return this.fullName + " says hello!";
  }
  get fullName() : string {
    return this.firstName + " " + this.lastName;
  }
}

また、図 4 で示すように、NormalPerson というサブタイプを (管理者およびプログラマ向けに) 作成するのも同様に簡単です。親クラスに従うコンストラクターを作成し、各職務に合ったメッセージを返すように greet メソッドをオーバーライドします。

図 4 Programmer の実装

class Programmer extends NormalPerson {
  constructor(fn: string, ln: string) {
    super(fn, ln);
  }
  greet() : string {
    return this.fullName + " says Hello, World!";
  }
}
class Manager extends NormalPerson {
  constructor(fn: string, ln: string) {
    super(fn, ln);
  }
  greet() : string {
    return this.fullName + " says let's dialogue about common synergies!";
  }
}

前述のとおり、型記述子 (およびインターフェイス宣言自体) を除いて、この実装は ECMAScript 2015 構文によく似ています。しかし、TypeScript の型チェックにより、コンストラクターに対して文字列以外をパラメーターとして使用しようとすると確実に拒否されます。ただし、クラスがエクスポートされないため、クライアント コードでは実際の実装を把握しません。クライアントが把握するのは、Person インターフェイスでクライアントが使用できる 3 つのプロパティ (firstName、lastName、および fullName) と 1 つのメソッド (greet メソッド) が定義されていることだけです。

装飾

TypeScript の注目すべき機能の最後として、デコレーターを取り上げます。これは、ECMAScript (ついでに言えば TypeScript) の実験的な機能で、カスタム属性に似ていますが、動作はまったく異なります。基本的には、先頭に @ を付けると、異なるコード コンストラクターが呼び出されても常に呼び出される関数を定義できます。この関数は、クラスが構成されたとき、メソッドが呼び出されたとき、プロパティにアクセスされたとき (または変更があったとき)、パラメーターがメソッドや関数の呼び出しの一部として渡されたときに呼び出されます。TypeScript はアスペクト指向プログラミング (AOP) 手法を利用しようとしているのは間違いありません。また、AngularJS 2 ではこの手法を非常によく利用します。

AOP ライブラリの典型的な例はログ関数の呼び出しです。特定の関数またはメソッドが呼び出されるたびに、呼び出し元に関係なく、コンソールにログを記録するコードを再利用する場合があります。当然、これはコード全体に関連する機能で、従来のオブジェクト指向を利用しないコード群では、継承などのコンストラクトを再利用します。TypeScript を使用すると、ログ デコレーターを作成し、このデコレーターでメソッドを装飾して、ログ動作を必要とするメソッドを指定できます。このログ動作は、装飾したメソッドが呼び出されるたびに呼び出されます。

実際には以下のようにログ デコレーターを作成した場合、返される Person 実装は greet メソッドで @log を使用でき、コンソールに呼び出しのログが記録されることになります。

import log from './log';
// ... Code as before
class Manager extends NormalPerson {
  constructor(fn: string, ln: string) {
    super(fn, ln);
  }
  @log()
  greet() : string {
    return this.fullName + " says let's dialogue about common synergies!";
  }
}

このコードを実行すると、メソッド レベルで適切なログが作成されます。

$ node app.js
Call: greet() => "Ted Neward says Hello, World!"
Ted Neward says Hello, World!
Call: greet() => "Andy Lientz says let's dialogue about common synergies!"
Andy Lientz says let's dialogue about common synergies!
Call: greet() => "Charlotte Neward says hello!"
Charlotte Neward says hello!

log コンポーネント自体はランタイム風の優れた要素ですが、今回は詳しく取り上げません。詳細なコードを図 5 に示しますが、そのしくみには触れません。1 つ言えるのは、TypeScript がコードを適切な場所に効果的に挿入することで、ログ デコレーターによって返される関数に対する呼び出しが行われます。

図 5 ログ注釈の定義

export default function log() {
  return function(target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor)
  {
    // Save a reference to the original method
    var originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {
      var argsLog = args.map(a => JSON.stringify(a)).join();
      var result = originalMethod.apply(this, args);
      var resultLog = JSON.stringify(result);
      console.log(`Call: ${propertyKey}(${argsLog}) => ${resultLog}`);
      return result;
    }
    // Return edited descriptor instead of overwriting
    // the descriptor by returning a new descriptor
    return descriptor;
  }
}

デコレーターの作成方法について知りたい方は、TypeScript Web サイトのデコレーターの説明 (bit.ly/2fh1lzC、英語) をご覧ください。さいわい、デコレーターを独自に作成する方法を知らなくても、AngularJS 2 は効果的に使用できます。ただし、既存のデコレーターの使用方法は必ず理解しておく必要があります。AngularJS 2 ではまず、AngularJS の公開以来、「Angular 手法」の主な要素となっている依存関係の挿入でデコレーターを使用します。

まとめ

今回、TypeScript について簡単に取り上げました。徹底的に説明したわけではありませんが、AngularJS 2 を取り入れて使用を開始したら順調に開発を進めることができるでしょう。ここで説明した機能の一部には特定のコンパイラ スイッチが必要です。特に、デコレーターではコンパイラ スイッチとして experimentalDecorators (または tscconfig.json での等価なスイッチ) が必要です。ただし、ほとんどの場合、Yeoman で生成されるスキャフォールディングによって適切なスイッチが配置されます。そのため、AngularJS 2 開発者はスイッチについて気にする必要はありません。

そういえば、AngularJS のコンポーネント、モデル、およびビューについて説明しようと考えていました。これについては次回のコラムまでお待ちください。それまでコーディングに励んでください。


Ted Neward は、シアトルを拠点に活躍している、ポリテクノロジーに関するコンサルタント、講演者、および指導者です。これまでに 100 本を超える記事を執筆している Ted は、F# MVP であり、さまざまな書籍を執筆および共同執筆しています。仕事への協力を依頼する場合、連絡先は ted@tedneward.com (英語のみ) です。また、tedneward.com (英語) でブログを公開しています。

この記事のレビューに協力してくれた技術スタッフの Shawn Wildermuth に心より感謝いたします。