クラス・インターフェース・モジュールは大規模な開発を行う際の強力な味方であり、TypeScriptの重要な機能の一つです。連載第3回目となる今回は、オブジェクト指向に欠かせないクラスについて紹介します。
クラス
JavaScriptはプロトタイプベースの言語であり、静的なクラスではありません。しかし、多くのライブラリにて静的なクラスを再現する機能が実装され、利用されています。TypeScriptでは、静的なクラスが使用でき、多くのクラスベースの言語と同様のノウハウや実装方法を利用できます。
クラスを定義するには、 classキーワードを使用します。
1 2 3 4 5 6 7 8 9 10 11 |
class Person { private name: string; constructor(name: string){ this.name = name; } introduceOnSelf(): string{ return "I'm " + this.name + "."; } } var tom = new Person("Tom"); tom.introduceOnSelf(); // "I'm Tom." |
アクセサは privateと publicが指定できます。アクセサを表記しない場合は publicとして扱われます。アクセサはTypeScript内でのみ有効で、コンパイル時のチェックにのみ使用されるため、 privateを指定しても出力されるJavaScriptコードでプライベートが実現されているわけではありません。あくまでも、TypeScript内でのみ適用されるため、出力されるコードがむやみに複雑になることもありません。
コンストラクタは constructorというメンバ関数内に記述します。また、コンストラクタのパラメータにアクセサを記述することで、プロパティ宣言と代入文を省略することもできます。
先のコードを省略すると次のようにスッキリと記述できます。
1 2 3 4 5 6 |
class Person { constructor(private name: string){} introduce(): string{ return "I am " + this.name + "."; } } |
静的なメンバは staticキーワードを使用して定義します。静的なメンバは作成したクラスのメンバとなるため、 thisではアクセスすることはできないので注意しましょう。
1 2 3 4 |
class Some{ static className = "Some"; } Some.className; |
セッターやゲッターも定義することができます。それぞれ、 set, getと修飾子を付け、メンバ関数と同様に宣言します。セッターのパラメータの型とゲッターの戻り値の型は一致している必要があります。
1 2 3 4 5 6 7 8 9 |
class Some { private property: string; set Property(value: string) { this.property = value; } get Property(): string { return this.property; } } |
ただし、セッター・ゲッターを利用するためにはコンパイルターゲットをECMAScript 5に指定する必要があります。本連載で使用しているSublime TextのT3Sプラグインの設定では ecmascript_targetを ES5に設定する必要があります。
もちろん、クラスを継承し、拡張したクラスを宣言することもできます。継承には extendsキーワードを使用します。
1 2 3 4 5 6 7 8 9 10 11 12 |
class Parent{ constructor(){} method():void; } class Child extends Parent{ constructor(){ super(); } method():void{ super.method(); } } |
オーバーライド時に親のメンバ関数を呼ぶ必要がある場合は、 superキーワードを使用し、親クラスのメンバを参照することができます。
インターフェース
クラスと同様にインターフェースも利用することができます。TypeScriptでは、インターフェースはクラスへメンバの実装を強制する際に使用されることはもちろんのこと、単に型としても利用されます。
インターフェースの定義には interfaceキーワードを使用し、クラスに実装を強制する際には implementsキーワードを使用します。
1 2 3 4 5 6 7 8 9 |
interface ISample{ property: string; method(): void; } class SampleClass implements ISample{ property = "property"; method(): void{ } } |
インターフェース ISampleの実装が強制されていため、 ISampleのメンバすべてが実装されていない場合はコンパイルエラーになります。
JavaScriptでは、多くのパラメータをメソッドに渡す際にオブジェクトにパラメータをまとめる手法をよくとります。TypeScriptでは、このような型の定義にインターフェースが頻繁に利用されます。
次のコードの func関数は多くのパラメータを受け取る関数です。 func関数は、このパラメータを1つのオブジェクトにまとめて、 paramsとして受け取ります。このとき、 paramsは any型ではない型を与えるべきであり、この型を作成するために、TypeScriptでは interfaceが頻繁に利用されます。次のコードでは ManyParamsインターフェースとなります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
interface ManyParams{ param1?: string; param2?: number; param3?: boolean; // ... paramn?: string; } function func(params: ManyParams): void { ... } func({ param1: "param1" , param3: true }); |
jQueryのAjaxを例にとると次のようになり、引数をインターフェースにすることでパラメータのメタ情報を取得できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
interface JQueryAjaxSettings { url?: string; data?: any; type?: string; // ... more properties; } /** * jQueryのajaxメソッドはJQueryAjaxSettings型の引数をとる。 */ $.ajax({ url: "/api/values" , type: "get" }) |
TypeScriptでは第1回で紹介したとおり、構造的部分型を採用しているため、インターフェース JQueryAjaxSettingsのインスタンスを生成する必要はなく、オブジェクトを利用できます。
また、TypeScriptのインターフェースでは、関数の型も定義することができ、TypeScriptで使用する型全般にユニークな呼称を定義できるものと言えます。
1 2 3 4 5 |
interface Listener{ (ev: Event):void; } var l: Listener = function (ev: Event){ } |
ジェネリック型
クラスやインターフェースの力を十分に発揮するには、ジェネリック型(総称型)が必要不可欠です。
TypeScriptは静的型付け言語であり、その恩恵を十分に受けるには、極力Any型の利用を避ける必要があります。すると、あらゆる型に対応したクラスとインターフェースを用意するには、おのずとジェネリック型という仕組みを利用する必要があります。
ジェネリック型を利用することで、データ型に依存することなく、構造や振る舞いを記述することができるようになります。
ジェネリック型を利用する際は、 <>を使用し、型を指定します。
次のコードは、ジェネリック型を利用したスタックのコードです。スタックは pushと popのメソッドが存在すればスタックであるとすると、 IStackインターフェースのように表現できます。この時、スタックが扱うデータの型をジェネリック型として Tで記述します。 ジェネリック型は型注釈する際にデータ型を指定することができ、 string型のデータを扱うスタックであれば、 IStackと注釈できます。この時、 Tは全て stringであると解決されます。
pushメソッドのパラメータは Tであるため、このインスタンスでは stringとなり、コンパイラにより型がチェックされます。
1 2 3 4 5 6 7 |
interface IStack<T> { push(item: T): void; pop(): T; } var stack: IStack<string> = []; stack.push("stack"); // ok stack.push(0); // error |
また、ジェネリック型にはごく簡単な上限制約を設けることができます。制約に合致しない場合はコンパイルエラーとなります。
1 2 3 4 5 6 |
interface IComparable { compareTo<T extends IComparable>(item: T): number; } function min<T extends IComparable>(a: T, b: T): T { return a.compareTo(b) < 0 ? a : b; } |
a.compareTo(b)のようにパラメータから型推測をするので、型の明示的な指定をほとんど記述することなく使用できます。
先ほど、ごく簡単な上限制約と紹介したのは、次の <T extends IComparable>ようにジェネリック型の Tを上限型の宣言に使用できないためです。 多くのジェネリック型を取り入れている言語では使用できるため、つまづきやすいポイントと言えます。
1 2 3 4 5 6 7 |
interface IComparable<T>{ compareTo(item: T): number; } // エラー: Tが上限型のジェネリックに指定されている function min<T extends IComparable<T>>(a:T, b:T): T{ return a.compareTo(b) < 0 ? a : b; } |
実際は、TypeScriptは制限的部分型であるため、上限制約を定義せずとも、先のようなロジックは利用できるためあまり使用されることはなく、むしろ、制約を設けるよりも制限的部分型を大いに利用するような仕様であるといえます。
内部モジュール
TypeScriptには内部モジュールと外部モジュールの2つのモジュール機能があり、プログラムを機能ごとに切り分けることができます。 内部モジュールはこれを利用することで、名前空間を分けることができます。名前空間を分けることでより構造化してアプリケーションを構築できるようになります。 モジュールを利用するには moduleキーワードを利用し、次のように記述します。 モジュールを外部スコープから参照可能にする場合は、 exportキーワードを使用します。
1 2 3 4 5 6 |
module M{ class Private{} export class Public{} } new M.Public(); // ok new M.Private(); // error |
モジュールはネストすることができます。このとき、ネストされたモジュールを参照可能にするには、やはり exportキーワードを使用します。
1 2 3 4 5 6 |
module M{ export module N{ export class O{} } } new M.N.O(); |
先のネストされたモジュールは、次のように .で区切り、短縮して書くことができます。
1 2 3 4 |
module M.N{ export class O{} } new M.N.O(); |
また、モジュールは分割して記述することができます。
1 2 3 4 5 6 7 8 |
module M{ export class A{} } module M{ export class B{} } new M.A(); new M.B(); |
モジュールのネストが深くなると、参照する際に記述量が多くなりますが importキーワードを使用して短い別名をつけ、短縮して使用することができます。
1 2 3 4 5 6 |
module Long.NameSpaces{ export class C{} } new Long.NameSpaces.C(); import C = Long.NameSpaces.C; new C(); |
まとめ
TypeScriptでは、他のクラスベースの言語とほとんど差異なく、静的なクラスを使用できます。 また、TypeScriptではインターフェースが非常に広範囲で活躍します。プリミティブな型以外のほとんどの型はインターフェースで表現可能です。JavaScriptコードをTypeScriptに置き換える際には、プログラム内で利用する型をインターフェースで整理してみましょう。
次回は、ファイル分割や外部モジュール、jQueryなどのJavaScriptライブラリを、TypeScriptで使用する方法を紹介します。
コメント