次世代JavaScript系言語「TypeScript」の主要言語機能:特集:TypeScript(プレビュー版)概説(後編)(1/2 ページ)
TypeScriptの基本的な言語仕様をコード例で示しながら解説。今回は「クラスの継承」「インターフェイス」「モジュール」。
前編では、TypeScriptの特徴と、クラスの基礎について説明した。引き続き今回は、クラスの継承について説明し、TypeScriptの主要な言語仕様である「インターフェイス」と「モジュール」について説明する。
前編ではバージョン「0.8.1」を使ったが、前編の公開後に新バージョン「0.8.2」が公開されたので、今回はこの新バージョンを使う(「0.8.2」の新機能については、「TypeScript 0.8.2 リリースと変更点 - THE TRUTH IS OUT THERE - Site Home - MSDN Blogs」を参照されたい)。
まずは、前回の積み残しであるクラスの機能の1つ「継承(heritage)」について説明する。
クラスの継承
ご存じのとおり、継承とは、元となる「基本クラス(base class)」の機能を継承したうえで、さらに新機能の追加や既存機能の置き換えを「派生クラス(derived class)」に実装することである。次のTypeScriptコードは、その継承の一例だ。
// 基本クラスの宣言
class Furniture {
constructor(public name: string, public price: number) {
this.setMessage();
}
private message: string; // privateインスタンス・メンバ
showMessage() {
alert(this.message);
}
setMessage() {
// 現在のインスタンスのメンバにアクセスするには、thisキーワード
this.message = this.name + "の価格は、" + this.price + "円です。";
}
public static getCounter(): string {
return "クラスがインスタンス化された回数: ";
}
}
// 派生クラスの宣言
class Chair extends Furniture {
constructor(nameArg: string, priceArg: number) {
super(nameArg, priceArg); // 基本クラスのコンストラクタの呼び出し
}
// 新機能の追加
changePrice(priceArg: number) {
this.price = priceArg; // 現在のインスタンスのメンバにアクセス
//super.price = 100; // 基本クラス・インスタンスのメンバにアクセス
//super.message = ""; // privateメンバにはアクセスできない
super.setMessage(); // this.setMessage(); でもOK
Furniture.getCounter(); // Chair.getCounter(); にはできない
}
// 既存機能の置き換え
//showMessage() {
// alert("オーバーライドしました。");
//}
}
var pipeChair = new Chair("パイプ椅子", 2000);
pipeChair.changePrice(3100); // 派生クラスのインスタンス・メンバにアクセス
pipeChair.showMessage(); // 基本クラスのインスタンス・メンバにアクセス
このサンプル・コードの実行結果は、前回のものとあまり変わらないので割愛する。このコード内容を簡単に説明しよう。
●基本クラスと派生クラスの定義
「Furniture」という基本クラスと、「Chair」という派生クラスを定義している。Chairクラスの記述を見ると分かるように、クラスを継承するには「extends」というキーワードを使って、
class <派生クラス名> extends <基本クラス名> { ... }
という形で宣言すればよい。その派生クラス内では、(上記のコード例では「changePrice」関数のように)新しいメンバ関数やメンバ変数を追加したり、(上記のコード例ではコメントアウトされている「showMessage」関数のように)既存の機能を置き換えたりできる。
○superキーワードと、thisキーワード
派生クラス「Chair」のコンストラクタ(constructor)の中を見ると、「super」という関数が呼び出されているが、これは基本クラス(=スーパー・クラス)のコンストラクタを呼び出すためのものだ(スーパー・コール: Super Calls)。
また、changePrice関数の中を見ると、メンバ変数やメンバ関数の前に「this.」や「super.」などが付いている。これらのキーワードは次のような意味を持つ。
- thisキーワード: 現在のインスタンスのメンバにアクセスする
- superキーワード: 基本クラスのインスタンス・メンバにアクセスする。ただしprivateメンバにはアクセスできない
例えば「this.price」というコードは、“現在のインスタンス”のメンバ変数「price」に値を設定しているというわけだ。
ここで注意が必要なのは、C#などのほかのオブジェクト指向言語の挙動とは異なり、「現在のインスタンス(this)」と「基本クラスのインスタンス(super)」は全く同じものではないということだ。そのため例えばchangePrice関数内で、
this.price = 100;
super.price = 200;
alert("価格: "+ this.price);
というコードを記述した場合、「価格: 100」というメッセージボックスが表示されることになる。
以上でクラスの説明は終わりだ。次にインターフェイスを説明する。
インターフェイス
継承とインターフェイス(interface)は、「2つのものを組み合わせる」という特徴が似ているので、オブジェクト指向言語の初心者が迷いやすい機能である。では両者の違いは何か? 「インターフェイスは、中身の実装を行わずに“型だけ”を定義できる」という点が、クラスの継承とは異なる、インターフェイスの重要なポイントだ。この特徴を生かして、同じ型を持つオブジェクトを交換して使えたりする。
●インターフェイスの宣言と実装
次のコードは、「IEngine」というインターフェイス(=型)を宣言して、それを実装(implements)した「Bike」というクラスを宣言している。このように、インターフェイスを実装するには、「implements」キーワードを用いる。
// インターフェイスの宣言
interface IEngine {
maxSpeed: number; // メンバ変数の定義
move(speed: number): { message: string; }; // メンバ関数の定義
}
// インターフェイスの実装
class Bike implements IEngine {
maxSpeed: number = 80;
move(speed: number) {
return {
message: (speed > this.maxSpeed) ?
(speed + "km! バイクはスピード違反です。") :
("バイクは時速「" + speed + "km」で進んでいます。")
};
}
}
まず「IEngine」インターフェイスの宣言内容を見ると、「maxSpeed」というメンバ変数と、「move」というメンバ関数が宣言されている。よって、このインターフェイスを実装するクラスには、必ずこれらのメンバを実装しなければならない。
ちなみに、次の例のようにメンバの識別子(Identifier)の後に「?」を付け加えると、そのメンバは実装しても、しなくてもよいことになる。
maxSpeed?: number;
move?(speed: number): { message: string; };
上記のmove関数の定義内容についてもう少し詳しく説明しておくと、関数の戻り値の型として指定されている「{ message: string; }」は、オブジェクト型リテラル(Object Type Literals)で表現したもので、この場合、messageプロパティ(string型)をメンバに持つ(型の)オブジェクトを戻り値として返却しなければならないことを意味している。実際に、Bikeクラスのmove関数では、「return { message: <値> };」というオブジェクト・リテラル(Object Literals)で戻り値が返却されている。ちなみに上記のコードで、その<値>のところには、「(条件式) ? <trueのときの値> : <falseのときの値>」という条件分岐が記述されている。
【コラム】“関数の型”を表現する方法
上記のmove関数は「関数シグネチャ(Function Signatures)」と呼ばれる型の表現方法だが、このほかにも「関数型リテラル(Function Type Literals)」と呼ばれる表現方法を取ることもできる。具体的には次のような書き方になる。
move: (speed: number) => { message: string; }; // 関数型リテラル
move(speed: number): { message: string; } // 関数シグネチャ
「Bike」クラスの方は、特に難しいところはないと思うので説明を割愛する。
○複数のインターフェイスの実装
TypeScriptでは、1つのクラスで複数のインターフェイスを実装することもできる。次のコードは、上記のコードに続けて、「ICarBody」インターフェイスを宣言し、「IEngine」と「ICarBody」の2つのインターフェイスを実装する「Car」クラスを宣言している例だ(コードの説明は割愛)。
……上記のコードの続き……
interface ICarBody {
color: string;
}
class Car implements IEngine, ICarBody {
maxSpeed: number = 120;
color: string;
move(speed: number) {
return {
message: (speed > this.maxSpeed) ?
(speed + "km! 自動車はスピード違反です。") :
("自動車は時速「" + speed + "km」で進んでいます。")
}
}
}
○同じインターフェイスを持つ、異なるオブジェクトの操作
以上のコードで、BikeクラスとCarクラスがともにIEngineインターフェイスを実装したことになる。つまり、これらのクラスのオブジェクトは、IEngineオブジェクトとしても扱えるということだ。例えば次のコードでは、IEngine型の変数「vehicle」に、BikeオブジェクトもしくはCarオブジェクトを代入したうえで、vehicle変数のmove関数を呼び出している。
……上記のコードの続き……
var runSpeed = Math.floor(Math.random() * 130); // 0〜130kmの速度をランダムに決定
var vehicle: IEngine = (Math.floor(Math.random() * 9) > 5) ? <IEngine> new Bike() : <IEngine> new Car();
var retValue = vehicle.move(runSpeed);
alert(retValue.message);
上記コードの「<IEngine>」は、Bike型やCar型をIEngine型にキャストしている(その前の「(Math.floor(Math.random() * 9) > 5)」はランダムにtrueかfalseを条件分岐しているだけである)。
○インターフェイスの継承
前述したクラスの継承のように、インターフェイスでも「extends」キーワードによって継承が行える。「,」で区切れば、複数のインターフェイスを継承することもできる(※ちなみにクラスの場合は、複数のクラスを継承することはできない)。
// 基本インターフェイス1
interface IEngine {
maxSpeed: number;
move(speed: number): { message: string; };
}
// 基本インターフェイス2
interface ITurboEngine {
move(speed: number): { distance: number; };
}
// 派生インターフェイス
interface ICarEngine extends IEngine, ITurboEngine {
move(speed: number): { message: string; distance: number; };
}
●インターフェイスを「型定義」として利用
ここまでは、インターフェイスを実装する方法を説明したが、TypeScriptでは実装せずに直接、インターフェイスを型定義として利用することもできる。取りあえずコードを見た方が早いだろう(次のコードを参照)。
interface IBook {
title: string;
price: number;
}
function getBook(bk: IBook) {
alert("『" + bk.title + "』は" + bk.price + "円です。");
}
// オブジェクトの型の構造(この場合は「title」と「price」という2つのメンバ)が一致していればOK
getBook({ title: "赤毛のアン", price: 1200, digital: true });
上記のコードは、「IBook」インターフェイスを宣言して、そのインターフェイスの型をgetBook関数の第1パラメータの型として指定している。getBook関数内では、titleプロパティやpriceプロパティの値を取得している(※ここでは、プロパティ=インスタンス・メンバ変数)。また、getBook関数の呼び出し元では、オブジェクト・リテラルにより、動的にtitleプロパティとpriceプロパティとdigitalプロパティを持つオブジェクトを生成して、引数として渡している。
ここで「digitalプロパティがあるから、型が完全に一致していない」ということに気付いただろうか。TypeScriptでは、「あくまで構造的に互換性があるかどうか」だけを見て型チェックされる(この特性は「構造的部分型、構造的サブタイピング: Structural Subtyping」と呼ばれる)。titleプロパティとpriceプロパティがあるのでIBookインターフェイスの型と互換性があるため、上記のコードはエラーにはならない。
このように、オブジェクトが必要なプロパティを持っていることを保証したい場合にも、インターフェイスは活用できる。
続いて、最後のテーマである「モジュール」について説明しよう。
Copyright© Digital Advantage Corp. All Rights Reserved.