TypeScript

TypeScript について

Peter Vogel

いろいろな意味で、TypeScript について理解するには、その固有のメリットを考えてみることです。TypeScript の言語仕様では、TypeScript を「JavaScript の糖衣構文」と表現しています。この表現は真実で、おそらくこの表現があるから、この言語の対象ユーザー、つまり現在 JavaScript を使っているクライアント側の開発者がこの言語を手に取る重要な一歩になります。

したがって、TypeScript を理解する前に、JavaScript を理解しておく必要があります。実際、言語仕様書 (bit.ly/1xH1m5B、英語) では、多くの場面で、結果として出力される JavaScript コードの観点から TypeScript の構造が記載されています。ただし、JavaScript と機能を共有するとしても、TypeScript は独自の言語だ捉えることも同様に大切です。

たとえば、C# と同様、TypeScript はデータの型を指定する言語です。他にもさまざまな機能を備えていますが、IntelliSense サポートやコンパイル時チェックが可能です。C# と同様、TypeScript には にジェネリックやラムダ式 (またはそれらに等価なもの) があります。

しかし当然、TypeScript は C# ではありません。現在使用しているサーバー側の言語と TypeScript の共通部分を理解することも大事ですが、TypeScript の独特の部分を理解することも同じように大切です。TypeScript の型システムは C# とは異なりシンプルです。TypeScript は他のオブジェクト モデルについて理解したことを独自の方法で活かし、C# とは異なる方法で継承を行います。さらに TypeScript は JavaScript にコンパイルするため、C# とは違って、基礎部分の多くを JavaScript と共有します。

疑問として残るのは、「TypeScript と JavaScript のどちらでクライアント側のコードを書くべきか」ということです。

TypeScript はデータの型を指定する

変数の宣言に使用できる TypeScript 組み込みのデータ型はそれほど多くなく、文字列、数値、ブール値だけです。この 3 種類の型は、(変数を宣言するときにも使用できる) any 型のサブタイプです。この 4 つの型で宣言した変数を、型 null または undefined に対して設定またはテストすることができます。また、メソッドを void で宣言して、値を返さないことを示すこともできます。

次の例は変数を文字列として宣言します。

var name: string;

このシンプルな型システムを、列挙値や 4 種類のオブジェクト型 (インターフェイス、クラス、配列、関数) を使って拡張できます。たとえば、次のコードはインターフェイス (オブジェクト型の 1 種) を ICustomerShort という名前で定義します。このインターフェイスには Id というプロパティと CalculateDiscount というメソッドの 2 つのメンバーを含みます。

interface ICustomerShort
{
  Id: number;
  CalculateDiscount(): number;
}

C# と同様、変数や戻り値の型を宣言するときにこのインターフェイスを使用できます。次の例は変数を ICustomerShort 型として宣言します。

var cs: ICustomerShort;

オブジェクト型をクラスとして定義すると、インターフェイスとは異なり、実行可能なコードも含めることができます。次の例はプロパティとメソッド 1 つずつ備えた CustomerShort というクラスを定義します。

class CustomerShort
{
  FullName: string;
  UpdateStatus( status: string ): string
  {
    ...manipulate status... 
    return status;
  }
}

最新バージョンの C# と同様、プロパティを定義する際に実装コードを用意する必要はありません。単純に名前と型を宣言するだけです。クラスは 1 つ以上のインターフェイスを実装することができます (図 1 参照)。ここでは前述の ICustomerShort インターフェイスとそのプロパティを CustomerShort クラスに追加しています。

図 1 クラスへのインターフェイスの追加

class CustomerShort implements ICustomerShort
{
  Id: number;
  FullName: string;
  UpdateStatus(status: string): string
  {
    ...manipulate status...
    return status;
  }
  CalculateDiscount(): number
  {
    var discAmount: number;
    ...calculate discAmount...
    return discAmount;
  }
}

図 1 に示すように、TypeScript でインターフェイスを実装する構文は C# と同様シンプルです。インターフェイスのメンバーを実装する場合、インターフェイス名を関連クラスのメンバーに結び付けるのではなく、同じ名前のメンバーを追加するだけです。上記の例では ICustomerShort を実装するために、単純に Id と CalculateDiscount をクラスに追加しています。TypeScript ではオブジェクト型リテラルも使用できるようにしています。次のコードは、変数 cst を、プロパティとメソッドをそれぞれ 1 つ含むオブジェクト リテラルに設定します。

var csl = {
            Age: 61,
            HaveBirthday(): number
          {
            return this.Age++;
          }
        };

次の例は、オブジェクト型を使用して UpdateStatus メソッドの戻り値を指定しています。

UpdateStatus( status: string ): { 
  status: string; valid: boolean }
{
  return {status: "New",
          valid: true
         };
}

オブジェクト型 (クラス、インターフェイス、リテラル、配列) に加えて、関数のシグネチャを表す関数型を定義することもできます。次のコードは CustomerShort クラスを基に書き直した CalculateDiscount で、discountAmount という 1 つのパラメーターを受け取ります。

interface ICustomerShort
{
  Id: number;
  CalculateDiscount( discountAmount:
    ( discountClass: string, 
      multipleDiscount: boolean ) => number): number
}

このパラメーターは、2 つのパラメーター (1つは文字列、もう 1 つはブール値) を受け取り、数値を返す関数型を使って定義しています。C# の開発者であれば、構文の見た目がかなりラムダ式に似ていることがわかります。

このインターフェイスを実装するクラスは図 2 のようになります。

図 2 適切なインターフェイスを実装するクラス

class CustomerShort implements ICustomerShort
{
  Id: number;
  FullName: string;
  CalculateDiscount( discountedAmount:
    ( discountClass: string, 
      multipleDiscounts: boolean ) => number ): number
  {
    var discAmount: number;
    ...calculate discAmount...
    return discAmount;
  }
}

最新バージョンの C# と同様、TypeScript も変数を初期化している値から変数のデータ型を推測します。次の例では、TypeScript が 変数 myCust を CustomerShort 型と想定します。

var myCust= new CustomerShort();
myCust.FullName = "Peter Vogel";

C# と同様、インターフェイスを使って変数を宣言後、そのインターフェイスを実装するオブジェクトをその変数に設定できます。

var cs: ICustomerShort;
cs = new CustomerShort();
cs.Id = 11;
cs.FullName = "Peter Vogel";

最後に、型パラメーター (C# のジェネリックのようなもの) を使って、呼び出し側のコードから使用するデータ型を指定できるようにします。次の例では、クラスを作成するコードから ld プロパティのデータ型を設定できるようにします。

class CustomerTyped<T>
{
  Id: T;
}

次のコードは Id プロパティのデータ型を文字列に設定してから使用しています。

var cst: CustomerTyped<string>;
cst = new CustomerTyped<string>();
cst.Id = "A123";

クラス、インターフェイスなどのパブリック メンバーを分離して名前の競合を避けるには、C# の名前空間に似た上記の構造をモジュール内で宣言します。これらのアイテムを別のモジュールで使用できるようにするには、アイテムに export キーワードでフラグを付ける必要があります。図 3 のモジュールは 2 つのインターフェイスとクラスをエクスポートします。

図 3 2 つのインターフェイスと 1 つのクラスのエクスポート

module TypeScriptSample
{
  export interface ICustomerDTO
  {
    Id: number;
  }
  export interface ICustomerShort extends ICustomerDTO
  {
    FullName: string;
  }
  export class CustomerShort implements ICustomerShort
  {
    Id: number;
    FullName: string;
  }

エクスポートしたコンポーネントを使用するには、次の例のようにコンポーネント名にプレフィックスとしてモジュール名を付けます。

var cs: TypeScriptSample.CustomerShort;

または、TypeScript の import キーワードを使用してそのモジュールへのショートカットを作成してもかまいせん。

import tss = TypeScriptSample;
...
var cs:tss.CustomerShort;

TypeScript のデータ型指定には柔軟性がある

変数宣言が逆になっていること (変数名が先でデータ型が後) とオブジェクト リテラルを除けば、ここまでの内容はすべて C# プログラマーには見慣れたものです。ただし、実際には、TypeScript でデータ型指定はすべて省略可能です。仕様書には、データ型が「注釈」と記載されています。データ型を省略する (かつ、TypeScript がデータ型を推測しない) 場合、データ型は既定の any 型になります。

TypeScript では、厳格にデータ型が一致することも求められません。TypeScript の仕様書では「構造的部分型」という表現で、互換性を判断しています。これはいわゆる「ダック タイピング」に似ています。TypeScript では、2 つのクラスが同じ型のメンバーをもつ場合、同じものと見なされます。たとえば、次のような ICustomerShort というインターフェイスを実装する CustomerShort クラスがあるとします。

interface ICustomerShort
{
  Id: number;
  FullName: string;
}
class CustomerShort implements ICustomerShort
{
  Id: number;
  FullName: string;
}

以下は、前述の CustomerShort クラスに似た CustomerDeviant というクラスです。

class CustomerDeviant
{
  Id: number;
  FullName: string;
}

構造的部分型により、CustomerShort クラスまたは ICustomerShort インターフェイスで定義した変数を、CustomerDevient に使用できます。次の 2 つの例は CustomerShort または ICustomerShort として宣言した変数と交換可能なかたちで、CustomerDeviant を使用しています。

var cs: CustomerShort;
cs = new CustomerDeviant
cs.Id = 11;
var csi: ICustomerShort;
csi = new CustomerDeviant
csi.FullName = "Peter Vogel";

この柔軟性により、TypeScript オブジェクト リテラルをクラスまたはインターフェイスとして宣言した変数に代入できるようになります。ただし、両者に構造的な互換性がある場合です。この例を以下に示します。

var cs: CustomerShort;
cs = {Id: 2,
      FullName: "Peter Vogel"
     }
var csi: ICustomerShort;
csi = {Id: 2,
       FullName: "Peter Vogel"
      }

この例は、代入可能性についての一般的な問題を引き起こす、見かけの型、スーパータイプ、サブタイプにまつわる TypeScript 固有の機能に踏み込んでいますが、ここでは取り上げません。このような機能により、たとえば、CustomerDeviant が今回のサンプル コードでエラーを起こさずに、CustomerShort に存在しないメンバーを持つことができます。

TypeScript にはクラスがある

TypeScript の仕様書では、この言語のことを「オブジェクト指向の継承メカニズムに関して多様性を実装するためにプロトタイプ チェーン (を用いる) クラス パターン」の実装と表現しています。実際のところ、この表現は TypeScript はデータ型を指定するだけでなく、事実上オブジェクト指向であることを示しています。

C# インターフェイスが基本インターフェイスから継承できるのと同様に、TypeScript インターフェイスは別のインターフェイスを拡張でき、かつそのインターフェイスが異なるモジュールで定義されていてもかまいません。次の例は、ICustomerShort インターフェイスを拡張して、ICustomerLong という新しいインターフェイスを作成しています。

interface ICustomerShort
{
  Id: number;
}
interface ICustomerLong extends ICustomerShort
{
  FullName: string;
}

ICustomerLong インターフェイスは、FullName と Id という 2 つのメンバーを含むことになります。マージ後のインターフェイスでは、継承元のメンバーが最初にきます。したがって、ICustomerLong インターフェイスは次のインターフェイスと等価です。

interface ICustomerLongPseudo
{
  FullName: string;
  Id: number;
}

ICustomerLong を実装するクラスには 2 つのプロパティが必要になります。

class CustomerLong implements ICustomerLong
{
  Id: number;
  FullName: string;
}

クラスも別のクラスから拡張できます。これは、インターフェイスを別のインターフェイスから拡張するのと同じ考え方です。図 4 のクラスは、CustomerShort を拡張し、その定義に新しいプロパティを追加します。プロパティを定義する場合 (特に便利な方法ではありませんが) 明示的なゲッターとセッターを使用します。

図 4 ゲッターとセッターを使って定義したプロパティ

class CustomerShort
{
  Id: number;
}
class CustomerLong extends CustomerLong
{
  private id: number;
  private fullName: string;
  get Id(): number
  {
    return this.id
  }
  set Id( value: number )
  {
    this.id = value;
  }
  get FullName(): string
  {
    return this.fullName;
  }
  set FullName( value: string )
  {
    this.fullName = value;
  }
}

TypeScript は、内部フィールド (id や fullName など) にクラスへの参照 (this) を使ってアクセスするベスト プラクティスを適用します。クラスには、C# では導入したばかりのフィールドの自動定義機能を含む constructor 関数を用意することもできます。TypeScript クラスの constructor 関数は名前付きのコンストラクターにする必要があります。その関数のパブリック パラメーターは自動的にプロパティとして定義され、パラメーターに渡される値により初期化されます。次の例では、コンストラクターが文字列型の Company というのパラメーターを 1 つ受け取ります。

export class CustomerShort implements ICustomerShort
{
  constructor(public Company: string)
  {       }

Company パラメーターはパブリックとして定義されているため、クラスにはコンストラクターに渡された値で初期化された Company というパブリック プロパティも用意されます。この機能により、変数 comp は以下の例のように "PH&VIS" に設定されます。

var css: CustomerShort;
css = new CustomerShort( "PH&VIS" );
var comp = css.Company;

コンストラクターのパラメーターをプライベートとして宣言すると、キーワード this によってクラスのメンバー内のコードからのみアクセスできる内部プロパティが作成されます。パラメーターをパブリックとしてもプライベートとしても宣言しなければ、プロパティは生成されません。

クラスにはコンストラクターが必須です。C# と同様、コンストラクターを用意しないと、自動的に提供されます。クラスを別のクラスから拡張する場合、作成するコンストラクターでは必ず super への呼び出しを含めます。これにより、拡張元のクラスのコンストラクターが呼び出されます。次の例では、super を呼び出して基本クラスのコンストラクターにパラメーターを渡すコンストラクターを含めています。

class MyBaseClass
{
  constructor(public x: number, public y: number ) { }   
}
class MyDerivedClass extends MyBaseClass
{
  constructor()
  {
    super(2,1);
  }
}

TypeScript の継承は異なる

繰り返しになりますが、一部の奇妙なキーワード (extends) を除けば、ここまでの内容はすべて C# プログラマーにとって見慣れたものです。ただし、こちらも繰り返しになりますが、クラスやインターフェイスの拡張は、C# の継承メカニズムとまったく同じではありません。TypeScript の仕様書では、拡張元のクラス (「基本クラス」) と拡張先のクラス (「派生クラス」) という一般的な用語が使われています。しかし、たとえば「継承」という言葉は「継承 (inheritance)」ではなく、クラスの「仕様の受け継ぎ (heritage specification)」と表現されています。

まず、基本クラスを定義する場合、TypeScript には C# ほど多くのオプションはありません。クラスやメンバーをオーバーライド不可、抽象、または仮想として宣言することはできません (ただし、インターフェイスは仮想基本クラスが提供する多くの機能を提供します)。

一部のメンバーが継承されないようにする方法はありません。派生クラスは、パブリック メンバーもプライベート メンバーも含めて、基本クラスのすべてのメンバーを継承します (基本クラスのパブリック メンバーはすべてオーバーライド可能ですが、プライベート メンバーはオーバーライドできません)。パブリック メンバーをオーバーライドする場合は、派生クラスで同じシグネチャでメンバーを定義するだけです。super キーワードを使用して派生クラスからパブリック メソッドにアクセスできますが、super を使用して基本クラスのプロパティにアクセスすることはできません (ただし、プロパティをオーバーライドすることは可能です)。

TypeScript では、同じ名前のインターフェイスと新しいメンバーを宣言するだけで、インターフェイスを拡張できるようにしています。これにより、新しい名前付きの型を作成しないで、既存の JavaScript コードを拡張することができます。図 5 の例では、2 つの異なるインターフェイス定義を使って ICustomerMerge インターフェイスを定義した後、クラスにそのインターフェイスを実装しています。

図 5 2 つのインターフェイス定義から定義される ICustomerMerge インターフェイス

interface ICustomerMerge
{
  MiddleName: string;
}
interface ICustomerMerge
{
  Id: number;
}
class CustomerMerge implements ICustomerMerge
{
  Id: number;
  MiddleName: string;
}

クラスは他のクラスを拡張できますが、インターフェイスを拡張することはできません。TypeScript では、インターフェイスはクラスも拡張できますが、継承を伴う方法でしか行えません。インターフェイスがクラスを拡張すると、そのインターフェイスはすべてのクラス メンバー (パブリックとプライベート) を含みますが、クラスの実装は含みません。図 6 では、ICustomer インターフェースは プライベート メンバー id、パブリック メンバー Id、およびパブリック メンバー MiddleName を含みます。

図 6 すべてのメンバー伴って拡張されたクラス

class Customer
{
  private id: number;
  get Id(): number
  {
    return this.id
  }
  set Id( value: number )
  {
    this.id = value;
  }
}
interface ICustomer extends Customer
{
  MiddleName: string;
}

ICustomer インターフェースには大きな制約があります。このインターフェイスは、そのインターフェイスが拡張された同じクラスを拡張したクラス (この場合は Customer クラス) でしか使用できません。TypeScript では、プライベート メンバーを派生クラスで実装し直すのではなく、そのインターフェイスが拡張するクラスから継承されるインターフェイスにプライベート メンバーを含める必要があります。ICustomer インターフェイスを使用する新しいクラスは、たとえば MiddleName の実装を提供する必要があることになります (これはそのインターフェイスで唯一指定されているため)。ICustomer を使用する開発者は、Customer クラスからパブリック メソッドを継承するか、オーバーライドするかのどちらかを選択できますが、プライベート id メンバーをオーバーライドすることはできません。

次の例は ICustomer インターフェイスを実装し、必要に応じて Customer クラスを拡張する NewCustomer というクラスを示しています。この例の NewCustomer は Customer から Id の実装を継承し、MiddleName の実装を提供しています。

class NewCustomer extends Customer implements ICustomer
{
  MiddleName: string;
}

インターフェイス、クラス、実装、拡張を組み合わせることによって、他のオブジェクト モデルで定義されたクラスを拡張するために定義するクラスを制御する方法が提供されます (詳細については、TypeScript 仕様書の 7.3 節「クラスを拡張するインターフェイス (Interfaces Extending Classes)」(英語) を参照してください)。この方法と、他の JavaScript ライブラリに関する情報を使用する TypeScript の機能を組み合わせて、それらのライブラリで定義されたオブジェクトを操作する TypeScript コードを作成できるようになります。

TypeScript は自作のライブラリを認識する

アプリで定義したクラスやインターフェイスに関する情報以外に、他のオブジェクト ライブラリに関する情報も TypeScript に提供することができます。その情報は、TypeScript の declare キーワードを使って処理されます。これにより、「アンビエント宣言」と呼ばれる仕様が生み出されます。ほとんどの JavaScript ライブラリの定義ファイルは DefinitelyTyped (英語) というサイトで見つかるため、必ずしも declare キーワードを使う必要はありません。その定義ファイルにより、TypeScript は事実上作業に必要なライブラリに関する「ドキュメンテーションを読み取り」ます。

「ドキュメンテーションを読み取る」ということは、当然、ライブラリを構成するオブジェクトを使用する際、データの型指定の IntelliSense サポートやコンパイル時チェックを利用できることになります。また、TypeScript により、特定の状況下であれば、変数が使用されているコンテキストからその型を推測できるようにもなります。次のコードでは、TypeScript と共に含められる lib.d.ts 定義ファイルによって、TypeScript は変数 anchor の型を HTMLAnchorElement と推測できます。

var anchor = document.createElement( "a" );

定義ファイルでは、createElement メソッドに文字列 "a" を渡したときに、そのメソッドから返される結果を指定します。anchor が HTMLAnchorElement ということがわかれば、TypeScript は anchor 変数がたとえば addEventListener メソッドをサポートすることを把握します。

TypeScript のデータ型の推測は、パラメーター型でも機能します。たとえば、addEventListener メソッドは 2 つのパラメーターを受け取ります。2 つ目のパラメーターは、addEventListener が PointerEvent 型のオブジェクトを渡す関数です。TypeScript はこれを把握し、その関数内での PointerEvent クラスの cancelBubble プロパティへのアクセスをサポートします。

span.addEventListener("pointerenter", function ( e )
{
  e.cancelBubble = true;
}

lib.d.ts が HTML DOM に関する情報を提供するのと同じ方法で、他の JavaScript の定義ファイルも同様の機能を提供します。たとえば、backbone.d.ts ファイルをプロジェクトに追加すれば、Backbone Model クラスを拡張するクラスを宣言し、以下のように独自のインターフェイスを実装できます。

class CustomerShort extends bb.Model implements ICustomerShort
{
}

Backbone と Knockout を TypeScript と併せて使う方法の詳細については、Practical TypeScript コラム (英語) を参照してください。2015 年は、TypeScript と Angular を併用することについて詳しく調べる予定です。

TypeScript は今回紹介したことがすべてではありません。TypeScript バージョン 1.3 では、共用体データ型 (特定の型のリストを返す関数をサポートする場合など) とタプルが含められる予定です。TypeScript チームは他のチームと連携し、JavaScript (Flow と Angular) にデータ型の指定を適用して TypeScript が可能な限り広い範囲の JavaScript ライブラリで確実に動作するよう取り組んでいます。

JavaScript でサポートされ、TypeScript ではサポートされないことを行う必要がある場合、いつでも JavaScript コードを統合することができます。これは、TypeScript が JavaScript のスーパーセットであるためです。この 2 つの言語のどちらでクライアント側のコードを記述すればよいか、という疑問の答えはもうわかったのではないでしょうか。


Peter Vogel は SOA、クライアント側開発、および UI デザインの専門技術を備え、Web 開発に特化した企業、PH&V Information Services の社長です。PH&V のクライアントはカナダ帝国商業銀行、ボルボ、マイクロソフトなどです。彼は Learning Tree International のコースで教鞭をとり、コースの執筆も行っています。さらに VisualStudioMagazine.com (英語) の Practical .NET コラムも担当しています。

この記事のレビューに協力してくれたマイクロソフト技術スタッフの Ryan Cavanaugh に心より感謝いたします。