JavaScript

TypeScript: .NET 開発者の JavaScript への抵抗をなくす

Shayne Boyer

 

Microsoft .NET Framework は、利用可能なツールを豊富に備えた非常に重要なプラットフォームであり、多くの投資の対象となっていることは間違いありません。C# や Visual Basic .NET の知識と XAML の知識を組み合わせれば、既存のスキルを活かす市場はほぼ無限に広がります。しかし、昨今では、長い間定評はありましたが、アプリケーション プラットフォームが整備されるにつれて、ここ数年で飛躍的に利用されるようになった言語について検討することも必要です。それが JavaScript です。JavaScript アプリケーションの成長は著しく、その能力も広大です。Node.js は、スケーラブルな JavaScript アプリケーション開発用プラットフォーム全体を指しますが、非常に一般的になり、Windows Azure 上でも開発可能です。さらに、JavaScript を HTML5 と組み合わせれば、ゲーム開発、モバイル アプリケーション、および Windows ストア アプリにも使用できます。

.NET 開発者にとっては、JavaScript の機能と市場での広がりは無視できないものになっています。この話をすると、同僚は JavaScript には厳密な型指定もクラス構造もなく、扱いが難しいという不満をよく漏らします。そのような意見には、JavaScript は関数型言語で、目的の処理を行うためのパターンが存在する、と切り返すことにしています。

さて、ここで検討すべきなのが TypeScript です。TypeScript は新しい言語ではなく、JavaScript のスーパーセットで、強力でかつ型指定が可能です。そのため、すべての JavaScript は TypeScript として有効であり、コンパイラが生成するのは JavaScript です。TypeScript はオープン ソース プロジェクトで、このプロジェクトに関するすべての情報は typescriptlang.org で参照できます。この記事の執筆時点の TypeScript はプレビュー バージョン 0.8.1 でした。

今回は、クラス、モジュール、および型を例に、TypeScript の基本的な考え方を説明し、.NET 開発者が JavaScript プロジェクトに抵抗なく取り組める方法を紹介します。

クラス

C# や Visual Basic .NET などの言語を使用する開発者にとって、クラスはなじみのある概念です。JavaScript では、クロージャやプロトタイプなどのパターンを使用してクラスの定義や継承を行います。TypeScript では、以前から使い慣れた構文を取り入れ、コンパイラがその構文を実現する JavaScript を生成します。以下の JavaScript スニペットを見てください。

var car;
car.wheels = 4;
car.doors = 4;

シンプルでわかりやすく見えますが、JavaScript ではオブジェクト定義に対するアプローチが厳密でないため、.NET 開発者は JavaScript を実際に使用しようとしません。この car オブジェクトは、それぞれのデータ型を認識せず、強制しないで新たなプロパティを後から追加できるため、実行中に例外がスローされることがあります。TypeScript クラス モデル定義ではこれがどのように変更され、car を継承、拡張するにはどうすればよいでしょう。図 1 の例について考えてみましょう。

図 1 TypeScript と JavaScript におけるオブジェクト

TypeScript JavaScript
class Auto{  wheels;  doors;}var car = new Auto();car.wheels = 2;car.doors = 4; var Auto = (function () {  function Auto() { }  return Auto;})();var car = new Auto();car.wheels = 2;car.doors = 4;

左は、wheels プロパティと doors プロパティを備え、しっかりと定義された car というクラス オブジェクトです。右は TypeScript コンパイラで生成された JavaScript で、ほぼ同じです。唯一の違いが Auto 変数です。

TypeScript エディターでは、新たなプロパティを追加するときに必ず警告が表示されます。「car.trunk = 1」のようなステートメントを使用するだけではだめで、コンパイラにより、"No trunk property exists on Auto" (Auto には trunk プロパティが存在しません) というエラーが表示されます。JavaScript が柔軟 (見方によっては "怠惰") であるため、このエラーの原因を特定する必要に迫られる開発者にとっては天の助けです。

コンストラクターは JavaScript でも使用できますが、TypeScript のツールによって、コンパイル時にオブジェクトの作成を強制するとか、呼び出しに適切な要素と型を渡さないとオブジェクトが生成されないなど、さらに強化されています。

クラスにコンストラクターを追加できるだけではなく、オプション パラメーターの作成、既定値の設定、プロパティ宣言の短縮などが可能です。TypeScript の強力さを示す例を 3 つ見てみましょう。

図 2 が 1 つ目の例です。wheels パラメーターと doors パラメーター (ここでは w と d で表されます) を渡してクラスを初期化するコンストラクターです。生成される JavaScript (右) はほぼ同じですが、アプリケーションでの動作や要件が増えると、違いが生じることがあります。

図 2 シンプルなコンストラクター

TypeScript JavaScript
class Auto{  wheels;  doors;  constructor(w, d){    this.wheels = w;    this.doors = d;  }}var car = new Auto(2, 4); var Auto = (function () {  function Auto(w, d) {    this.wheels = w;    this.doors = d;  }  return Auto;})();var car = new Auto(2, 4);

 

図 3 は、図 2 のコードを変更し、wheels パラメーター (w) の既定値を 4 に設定し、doors パラメーター (d) の右には疑問符を挿入してこのパラメーターをオプションにしています。先述の例のように、インスタンスのプロパティを引数に設定するパターンは一般的な手法で、"this" キーワードを使用します。

図 3 変更後のシンプルなコンストラクター

TypeScript JavaScript
class Auto{  wheels;  doors;  constructor(w = 4, d?){    this.wheels = w;    this.doors = d;  }}var car = new Auto(); var Auto = (function () {  function Auto(w, d) {    this.wheels = w;    this.doors = d;  }  return Auto;})();var car = new Auto(4, 2);

好んで使用する .NET 言語の機能は、コンストラクターのパラメーター名の前にパブリック キーワードを追加するだけで、クラスのプロパティを宣言できる機能です。private キーワードも使用可能で、同様に自動的に宣言されますが、クラスのプロパティは非公開になります。

既定値、オプション パラメーター、および型の注釈は、TypeScript の自動プロパティ宣言機能で拡張され、簡単に入力できるようになり、生産性が高まります。図 4 のスクリプトを比較すると、複雑さの違いがわかります。

図 4 自動宣言機能

TypeScript JavaScript
class Auto{  constructor(public wheels = 4,    public doors?){  }}var car = new Auto();car.doors = 2; var Auto = (function () {  function Auto(wheels, doors) {    if (typeof wheels ===      "undefined") {      wheels = 4; }    this.wheels = wheels;    this.doors = doors;  }  return Auto;})();var car = new Auto();car.doors = 2;

 

TypeScript ではクラスの継承も可能です。この Auto の例を使用して、基本クラスを拡張する Motorcycle クラスを作成します。図 5 では、drive 関数と stop 関数も基本クラスに追加しています。TypeScript に数行コードを追加するだけで、Auto を継承し、doors と wheels に適切なプロパティが設定された Motorcycle クラスが追加されます。

図 5 Motorcycle クラスの追加

class Auto{
  constructor(public mph = 0,
    public wheels = 4,
    public doors?){
  }
  drive(speed){
    this.mph += speed;
  }
  stop(){
    this.mph = 0;
  }
}
class Motorcycle extends Auto
{
  doors = 0;
  wheels = 2;
}
var bike = new Motorcycle();

ここで重要なことは、コンパイラが生成した JavaScript の先頭に、図 6 に示すように "___extends" という数行の関数があることです。このコードは、生成される JavaScript にのみ挿入されます。これは、継承機能を支援するヘルパー クラスです。なお、このヘルパー関数はソースに関係なくまったく同じシグネチャを持つため、複数ファイルの JavaScript を編成し、SquishIt や Web Essentials などのユーティリティを使用してスクリプトを組み合わせる場合、ユーティリティで重複する関数を調整する方法によっては、エラーが発生することがあります。

図 6 コンパイラが生成した JavaScript

var __extends = this.__extends || function (d, b) {
  function __() { this.constructor = d; }
  __.prototype = b.prototype;
  d.prototype = new __();
}
var Auto = (function () {
  function Auto(mph, wheels, doors) {
    if (typeof mph === "undefined") { mph = 0; }
    if (typeof wheels === "undefined") { wheels = 4; }
    this.mph = mph;
    this.wheels = wheels;
    this.doors = doors;
  }
  Auto.prototype.drive = function (speed) {
    this.mph += speed;
  };
  Auto.prototype.stop = function () {
    this.mph = 0;
  };
  return Auto;
})();
var Motorcycle = (function (_super) {
  __extends(Motorcycle, _super);
  function Motorcycle() {
    _super.apply(this, arguments);
    this.doors = 0;
    this.wheels = 2;
  }
  return Motorcycle;
})(Auto);
var bike = new Motorcycle();

モジュール

TypeScript のモジュールは、.NET Framework の名前空間に相当します。モジュールは優れた方法で、コードの整理やビジネス ルールとプロセスのカプセル化は、この機能があることで可能になります (JavaScript にこの機能を提供するための方法は組み込まれていません)。JQuery のモジュール パターン (動的名前空間) は、JavaScript の名前空間の中で最も一般的なパターンです。TypeScript のモジュールでは、構文が単純になり、同じ効果が得られます。Auto の例では、コードをモジュール内にラップして、Motorcycle クラスのみを公開します (図 7 参照)。

Example モジュールは基本クラスをカプセル化し、Motorcycle クラスは export キーワードをプレフィックスに付けることで公開しています。これにより、Motorcycle のインスタンスの作成と、そのメソッドすべての使用が可能になりますが、Auto 基本クラスは公開されません。

図 7 モジュール内にラップした Auto クラス

module Example {
  class Auto{
    constructor(public mph : number = 0,
      public wheels = 4,
      public doors?){
      }
      drive(speed){
      this.mph += speed;
      }
      stop(){
      this.mph = 0;
      }
  }
  export class Motorcycle extends Auto
  {
    doors = 0;
    wheels = 2;
  }
}
var bike = new Example.Motorcycle();

モジュールを使用するもう 1 つのメリットは、複数のモジュールをマージできることです。もう 1 つ Example という名前のモジュールを作成すると、TypeScript では、最初のモジュールのコードと新しいモジュールのコードは Example のステートメントを通じて、名前空間のように相互にアクセス可能であると見なされます。

モジュールはコードのメンテナンスと構成を容易にします。これを使用することで、大規模なアプリケーションのメンテナンスで開発チームにかかる負担が軽減されます。

JavaScript をほとんど使わない開発者が大きな不満の 1 つとして挙げるのは、タイプ セーフではないことです。TypeScript ではタイプ セーフ性を利用できます (TypeScript という名前はここからきています) 。タイプ セーフ性は、変数を文字列型やブール型で宣言するだけにとどまりません。

JavaScript では、x に foo を代入し、コードで後から x に 11 を代入することはまったく問題ありませんが、実行中に必ず発生する NaN の原因を特定しようとすると非常に神経を使うことがあります。

タイプ セーフ機能は、TypeScript の最大のメリットの 1 つです。用意されている型は string、number、bool、および any の 4 つです。図 8 に、変数 s の型を宣言する構文と、コンパイラによる生成後にその型で実行できる動作を識別する IntelliSense を示します。

An Example of TypeScript IntelliSense
図 8 TypeScript の IntelliSense の例

変数や関数の型指定機能のほかに、TypeScript には型を推定する機能があります。単純に文字列を返す関数を作成できます。このことを認識して、コンパイラとツールは型の推定を行い、戻り値で実行できる操作が自動的に示されます (図 9 参照)。

An Example of Type Inference
図 9 型の推論の例

このメリットは、開発者が推論しなくても戻り値が文字列だとわかることです。型の推論は、開発者が JQuery やドキュメント オブジェクト モデル (DOM) などのコードで、さまざまなライブラリを参照に利用する場合にとても役立ちます。

注釈で型システムを活用する方法もあります。元の Auto クラスでは、wheels と doors のみが宣言されていたことを思い出してください。ここでは、注釈により、car で Auto のインスタンスを作成するときに正しい型を設定できるようになります。

class Auto{
  wheels : number;
  doors : number;
}
var car = new Auto();
car.doors = 4;
car.wheels = 4;

ただし、生成される JavaScript では、注釈は削除されるため、無駄がなく、追加の依存関係を気にする必要はありません。ここでもメリットがあり、厳密な型指定に加え、通常は実行中に発生する単純なエラーを取り除くことができます。

インターフェイスは、TypeScript に備えられたタイプ セーフ性のもう 1 つの例です。インターフェイスでは、オブジェクトの形状を定義できます。図 10 では、travel という新しいメソッドが Auto クラスに追加され、Trip 型のパラメーターを受け取ります。

図 10 Trip インターフェイス

interface Trip{
  destination : string;
  when: any;
}
class Auto{
  wheels : number;
  doors : number;
  travel(t : Trip) {
  //..
  }
}
var car = new Auto();
car.doors = 4;
car.wheels = 4;
car.travel({destination: "anywhere", when: "now"});

正しい構造以外で travel メソッドを呼び出すと、デザイン時コンパイラがエラーを表示します。対照的に、このコードを JavaScript に入力した場合 (たとえば .js ファイルに入力した場合) は、アプリケーションを実行するまでこのようなエラーを発見することはほとんどできません。

図 11 に示すように、最初の開発者だけでなく、ソースをメンテナンスする後任の開発者にも、型の注釈の利用が非常に役立つことがわかります。

Annotations Assist in Maintaining Your Code
図 11 コードのメンテナンスに役立つ注釈

既存のコードとライブラリ

既存の JavaScript コードについてはどうでしょう。Node.js 上で構築している場合、または toastr、Knockout、JQuery などのライブラリを使用している場合はどうでしょう。TypeScript では、宣言ファイルを便利に使用できます。まず、すべての JavaScript が TypeScript で有効であることを思い出してください。このため、独自に作成したコードがあれば、それをデザイナーにコピーすると、コンパイラによってそれに合った JavaScript が生成されます。独自の宣言ファイルを作成することは、優れた選択肢です。

主要なライブラリとフレームワーク用には、Boris Yankov という男性 (Twitter: twitter.com/borisyankov、英語) により、GitHub (github.com/borisyankov/DefinitelyTyped、英語) に便利なリポジトリが作成されています。ここには、いくつかの最も一般的な JavaScript ライブラリ用に数多くの宣言ファイルが掲載されています。これはまさに、TypeScript チームが期待していたことです。ちなみに、Node.js の宣言ファイルは TypeScript チームが作成し、ソース コードの一部として使用できます。

宣言ファイルの作成

ライブラリの宣言ファイルを見つけられない場合や独自のコードを使用している場合は、宣言ファイルを作成する必要があります。JavaScript を TypeScript 側にコピーし、型定義を追加し、コマンド ライン ツールを使用して参照用の宣言ファイル (*.d.ts) を生成します。

図 12 に、JavaScript で成績の平均点を計算する単純なスクリプトを示します。スクリプトを左のエディターにコピーし、型の注釈を追加して、.ts 拡張子を付けてファイルを保存します。

図 12 宣言ファイルの作成

TypeScript JavaScript
function gradeAverage(grades : string[]) {  var total = 0;  var g = null;  var i = -1;  for(i = 0; i < grades.length; i++) {      g = grades[i];      total += getPointEquiv(grades[i]);  }  var avg = total / grades.length;  return getLetterGrade(Math.round(avg));}function getPointEquiv(grade : string) {  var res;  switch(grade) {    case "A": {      res = 4;      break;    }    case "B": {      res = 3;      break;    }    case "C": {      res = 2;      break;    }    case "D": {      res = 1;      break;    }    case "F": {      res = 0;      break;    }  }  return res;}function getLetterGrade(score : number) {  if(score < 1) {    return "F";  }  if(score > 3) {    return "A";  }  if(score > 2 && score < 4) {    return "B";  }  if(score >= 1 && score <= 2) {    return "C";  }  if(score > 0 && score < 2) {    return "D";  }} function gradeAverage(grades){  var total = 0;  var g = null;  var i = -1;  for(i = 0; i < grades.length; i++) {      g = grades[i];      total += getPointEquiv(grades[i]);  }  var avg = total / grades.length;  return getLetterGrade(Math.round(avg));}function getPointEquiv(grade) {  var res;  switch(grade) {    case "A": {      res = 4;      break;    }    case "B": {      res = 3;      break;    }    case "C": {      res = 2;      break;    }    case "D": {      res = 1;      break;    }    case "F": {      res = 0;      break;    }  }  return res;}function getLetterGrade(score) {  if(score < 1) {    return "F";  }  if(score > 3) {    return "A";  }  if(score > 2 && score < 4) {    return "B";  }  if(score >= 1 && score <= 2) {    return "C";  }  if(score > 0 && score < 2) {    return "D";  }}

次に、コマンド プロンプトを開き、TypeScript コマンド ライン ツールを使用して宣言ファイルと最終的な JavaScript を次のように作成します。

tsc c:\gradeAverage.ts –declarations

コンパイラは 2 つのファイルを作成します。gradeAverage.d.ts という宣言ファイルと、gradeAverage.js という JavaScript ファイルです。gradeAverage 機能を必要とする今後の TypeScript ファイルでは、エディターの先頭に次の参照を追加します。

/// <reference path="gradeAverage.d.ts">

このようにすると、このライブラリを参照するときに、すべての型指定とツールの機能が強調表示されます。これは、DefinitelyTyped GitHub リポジトリのすべての主要なライブラリで見られます。

宣言ファイル用にコンパイラが備えている便利な機能は、参照の自動スキャン機能です。この機能を使うと、jQueryUI の宣言ファイルを参照して jQuery を参照する場合、現在の TypeScript ファイルで、入力候補が表示されるメリットがあり、jQuery を直接参照しているかのように関数のシグネチャと型が表示されます。また、ソリューションで使用するすべてのライブラリへの参照を含んだ単一の宣言ファイル (たとえば "myRef.d.ts") を作成して、すべての TypeScript コードに参照を 1つだけにすることができます。

Windows 8 と TypeScript

Windows ストア アプリの開発では HTML5 が標準になっており、開発者はこの種のアプリで TypeScript が使えるかどうかに関心を寄せています。端的に言えば「使えます」ということになりますが、使用するためにはいくつかの設定が必要です。この記事の執筆時点では、Visual Studio インストーラーまたは他の拡張機能から使用できるこのツールは、Visual Studio 2012 の JavaScript Windows ストア アプリ テンプレートでは完全には有効になっていません。

typescript.codeplex.com (英語) で、3 つの重要な宣言ファイル (winjs.d.ts、winrt.d.ts、および lib.d.ts) を使用できます。これらのファイルを参照することで、カメラやシステム リソースなどにアクセスする環境で使用する WinJS や WinRT JavaScript ライブラリを使用できます。また、jQuery への参照を追加して、ここで説明した IntelliSense やタイプ セーフ機能を使用できます。

図 13 は、ユーザーの地理位置情報にアクセスしたり、Location クラスを設定したりするライブラリを使用している簡単な例です。このコードは、HTML の image タグを作成し、Bing Map API から静的な地図を追加します。

図 13 Windows 8 用宣言ファイル

/// <reference path="winjs.d.ts" />
/// <reference path="winrt.d.ts" />
/// <reference path="jquery.d.ts" />
module Data {
  class Location {
    longitude: any;
    latitude: any;
    url: string;
    retrieved: string;
  }
  var locator = new Windows.Devices.Geolocation.Geolocator();
  locator.getGeopositionAsync().then(function (pos) {
    var myLoc = new Location();
    myLoc.latitude = pos.coordinate.latitude;
    myLoc.longitude = pos.coordinate.longitude;
    myLoc.retrieved = Date.now.toString();
    myLoc.url = "http://dev.virtualearth.net/REST/v1/Imagery/Map/Road/"
      + myLoc.latitude + "," + myLoc.longitude
      + "15?mapSize=500,500&pp=47.620495,-122.34931;21;AA&pp="
      + myLoc.latitude + "," + myLoc.longitude
      + ";;AB&pp=" + myLoc.latitude + "," + myLoc.longitude
      + ";22&key=BingMapsKey";
    var img = document.createElement("img");
    img.setAttribute("src", myLoc.url);
    img.setAttribute("style", "height:500px;width:500px;");
    var p = $("p");
    p.append(img);
  });
};

まとめ

TypeScript により JavaScript 開発に追加される機能は多くはありませんが、通常の Windows アプリケーション開発で、使い慣れた言語で似た機能を開発している .NET 開発者にとっては大きなメリットになります。

TypeScript は万能の解決策ではありませんし、それを意図して作成されたものではありません。しかし、JavaScript の使用を躊躇している人にとっては、TypeScript は使用に対する抵抗を減らすすばらしい言語です。

Shayne Boyer は、Telerik MVP、Nokia Developer Champion、MCP、INETA の講演者であり、フロリダ州オーランドのソリューション アーキテクトでもあります。過去 15 年間に渡って、マイクロソフト ベース ソリューションの開発に携わっています。生産性とパフォーマンスに重点を置いて、大規模 Web アプリケーションに 10 年以上取り組んでいます。余暇には、Orlando Windows Phone and Windows 8 User Group を運営したり、tattoocoder.com (英語) で最新テクノロジに関するブログを執筆しています。

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