TypeScript/handbook抄訳① Basic TypesからClassesまで
TypeScriptの公式サイトにあるhandbookページのコンテンツを抄訳しました(2016/03/16取得)。訳の正確性については期待しないでください。文法に関する説明にはなるべく書かれている通りとなるよう注意を払っているつもりですが、それ以外の箇所については割愛や乱暴な意訳をしているところがたくさんあります。
まずはBasic TypesからClassesまでを掲載します。続きはまた次回。
* * *
基本型
JavaScriptで使える型はTypeScriptでも使える。これに加えてEnum
型も使える:
- Boolean
- Number
- String
- Array
- Enum
配列
配列は2つの型表記を持つ。T[]
とArray<T>
である。
列挙型
TypeScriptで標準データ型に加わった強力な型がEnum
である:
enum Color {Red, Green, Blue}; var c: Color = Color.Green;
Enum
のインスタンスはデフォルトでは0始まりの整数を値として持つ(というかEnum
はNumber
のサブ型であり、Enum
のインスタンスはNumber
のインスタンスに外ならない)。これを例えば1始まりにしたい場合は以下のようにする:
enum Color {Red = 1, Green, Blue}; var c: Color = Color.Green;
もちろんインスタンスごとに任意の値を指定してもよい。Enum
にはそのインスタンス(もしくは同じことだがインスタンスがあらわす整数)を元にその名称を得る方法も用意されている:
enum Color {Red = 1, Green, Blue}; var colorName: string = Color[2];
any
ある変数に代入する値の型やある関数の仮引数の型、戻り値の型が、アプリケーションのコーディングをしている時点ではわからないということがある。動的コンテンツやサードパーティ製ライブラリに由来する値がこれに該当する。TypeScriptでは、型チェックの意図的な放棄を選択することができる。そのためのラベルがany
である:
var notSure: any = 4; notSure = "数値じゃなくて文字列なんじゃないかな"; notSure = false; // うん。どう考えても真偽値だ。
any
は要素型の一定しない配列でも活躍する:
var list:any[] = [1, true, "free"]; list[1] = 100;
void
戻り値の存在しない関数にはそれをあらわす特別なラベルvoid
を使用する:
function warnUser(): void { alert("This is my warning message"); }
インターフェース
TypeScriptの中心的な原則の1つに値の持つ「かたち」に焦点を置いた型チェックがある。これはしばしば「ダックタイピング」もしくは「構造的部分型」と呼ばれるものである。インターフェースはこれらの型に名前をつける役割を持つ。インターフェースはまた、自プロジェクトのコードと他プロジェクトのコードの間での決まり事を表すのに貢献する。
はじめてのインターフェース
インターフェースがどのように機能するのか理解するために、わかりやすい例からはじめよう:
function printLabel(labelledObj: {label: string}) { console.log(labelledObj.label); } var myObj = {size: 10, label: "Size 10 Object"}; printLabel(myObj);
型チェッカーはprintLabel
関数の呼び出しをチェックする。この関数は仮引数を1つ持っており、その引数はlabel
というstring
型のプロパティを持つことが求められている。注意して見てほしいのは、実際に関数に渡されている値にはより多くのプロパティが存在していることだ。しかし型チェッカーは「少なくとも」必須とされているプロパティが存在していることしかチェックはしない。だから型はマッチしている。
同じ例を別のかたちで示そう。今度は値にlabel
というstring
型のプロパティが必要であることをインターフェース宣言を用いて示している:
interface LabelledValue { label: string; } function printLabel(labelledObj: LabelledValue) { console.log(labelledObj.label); } var myObj = {size: 10, label: "Size 10 Object"}; printLabel(myObj);
インターフェースにLabelledValue
という名前が付いたことで、プロパティに関する要求事項をその名前を使って示すことができる。注意して見てほしいのは、他の言語とはちがって、実際に渡される値は明示的にインターフェースを実装している必要はないということだ。まさにここが重要である。問題となっているのは「かたち」なのだ。引数として渡される値がなんであれ引数に求められている要求の一覧を満たすなら、それは妥当な引数なのである。
なお、型チェッカーはプロパティの順序は関知しない。要求された通りのプロパティが一式存在すること、それだけが要件となる。
オプションのプロパティ
インターフェースが持つプロパティのすべてが必須のものではないということもある。ある条件下でいくつかのプロパティは存在するが、残りは存在しないというようなケースだ。このようなプロパティのあり方は、とくに「オプションバッグ」と呼ばれる設計パターンの実現に用いられる。例えばこうだ:
interface SquareConfig { color?: string; width?: number; } function createSquare(config: SquareConfig): {color: string; area: number} { var newSquare = {color: "white", area: 100}; if (config.color) { newSquare.color = config.color; } if (config.width) { newSquare.area = config.width * config.width; } return newSquare; } var mySquare = createSquare({color: "black"});
オプションのプロパティには名前の末尾に?
をつけておく。
オプションのプロパティの優位性は対外的にはAPIのオプションとして指定可能な値とその型を示すことができるということであり、対内的には受け取ったオブジェクトからプロパティの値を取り出そうとしてミスタイプしたりする心配がないということである。次の例を見ていただきたい:
interface SquareConfig { color?: string; width?: number; } function createSquare(config: SquareConfig): {color: string; area: number} { var newSquare = {color: "white", area: 100}; if (config.color) { newSquare.color = config.collor; // 型チェッカーはここでエラーを報告する } if (config.width) { newSquare.area = config.width * config.width; } return newSquare; } var mySquare = createSquare({color: "black"});
関数型
TypeScriptのインターフェースはすでに見たプロパティを持つオブジェクトに加えて、関数の型をあらわすこともできる。それには仮引数リストと戻り値型からなる呼び出しシグネチャを使う。
interface SearchFunc { (source: string, subString: string): boolean; }
ひとたびインターフェースを定義したら他の場合と同じように使用できる。ここでは、関数型の変数を定義して関数型の値を代入してみよう:
var mySearch: SearchFunc; mySearch = function(source: string, subString: string) { var result = source.search(subString); if (result == -1) { return false; } else { return true; } }
関数型のチェックにおいて仮引数リストの仮引数名は一致している必要がない。仮引数の型とその順序だけが考慮される。また関数の戻り値型は、return
文で返されている値により暗黙のうちに示すことができる。今回の例でいればboolean
だ。そしてもし関数があるときにはstring
やnumber
を返していたりしたら、型チェッカーはそれがSearchFunc
の定義にマッチしないと警告するだろう。
配列型
関数型をインターフェースで表現できたように、配列型もまたそれができる。配列型の定義は添字の型とその添字を指定することで得られる値の型を伴う。
interface StringArray { [index: number]: string; } var myArray: StringArray; myArray = ["Bob", "Fred"];
添字の方にはstring
もしくはnumber
を使用できる。両方とも使用するという選択も可能だが、その場合number
型の添字を指定することで得られる値の型はstring
型の添字を指定することで得られる値の型のサブ型でないといけない。
添字シグネチャは辞書を表現するのにも役立つが、添字指定することで得られる値の型が合致していることが求められる。だから次の例では型チェッカーがエラーを報告する:
interface Dictionary { [index: string]: string; length: number; // エラー: 'length'の型は[string]が返す型のサブ型でない }
クラス型
インターフェースを実装する
C#やJavaにおけるインターフェースの一般的な用途といえば、クラスを特定の約束事に従わせることである。TypeScriptでもこれは可能である:
interface ClockInterface { currentTime: Date; } class Clock implements ClockInterface { currentTime: Date; constructor(h: number, m: number) { } }
インターフェースにはもちろんメソッドを含めることができる:
interface ClockInterface { currentTime: Date; setTime(d: Date); } class Clock implements ClockInterface { currentTime: Date; setTime(d: Date) { this.currentTime = d; } constructor(h: number, m: number) { } }
クラスのインスタンスの側とstaticの側の違い
クラスとインターフェースを利用するに際して、クラスは2つの型を持つという点を心に留めておこう: static
な側の型とインスタンスの側の型である。注意して聴いてほしい。仮にコントラクト・シグネチャとともにインターフェースを宣言したとして、そのインターフェースを実装するクラスを宣言しようとするとエラーが起こる:
interface ClockInterface { new (hour: number, minute: number); } class Clock implements ClockInterface { currentTime: Date; constructor(h: number, m: number) { } }
なぜだろうか? クラスがインターフェースを実装するとき、クラスのインスタンスの側の型のみが考慮される。ところでコンストラクタはstatic
な側に存在するものだから、この考慮のうちにない。こうしてクラスとインターフェースは矛盾をきたすからエラーが起きるのだ。
コントラクト・シグネチャを持つインターフェースは、クラスのstatic
な側を直接利用したい場合に活躍する。例えば次の例ではクラスを問題のインターフェースの型の変数に代入することで、クラスのインスタンスではなく、コンストラクタそのものを参照する:
interface ClockStatic { new (hour: number, minute: number); } class Clock { currentTime: Date; constructor(h: number, m: number) { } } var cs: ClockStatic = Clock; var newClock = new cs(7, 30);
インターフェースを拡張する
クラス同様、インターフェースは拡張できる。拡張元には複数のインターフェースを指定できる。
interface Shape { color: string; } interface PenStroke { penWidth: number; } interface Square extends Shape, PenStroke { sideLength: number; } var square = <Square>{}; square.color = "blue"; square.sideLength = 10; square.penWidth = 5.0;
複合型
JavaScriptは動的かつ柔軟な性質を持っており、これまで見てきた型のうち複数を組み合わせたように機能するものもある。次に見るのは関数とプロパティの集合としてのオブジェクト、いずれとしても機能する型の例である:
interface Counter { (start: number): string; interval: number; reset(): void; } var c: Counter; c(10); c.reset(); c.interval = 5.0;
サードパーティ製ライブラリを利用する場合、こうしたパターンに該当する型を利用することもあることだろう。
クラス
従来のJavaScriptでは関数とプロトタイプ・ベースの継承関係─これは再利用可能なコンポーネントから新たなそれが組み立てられるというものだが─に焦点が当てられていた。しかしこのことは、クラスが機能の継承を担いクラスからオブジェクトが組み立てられるという、クラス・ベースのオブジェクト指向プログラミングの手法に馴染んだプログラマには少しく厄介な状況であった。ところで、ECMAScript 6─次期バージョンのJavaScript─から、クラス・ベースのオブジェクト指向プログラミングによりアプリケーションを開発することが可能になる予定である。そしてTypeScriptでは現段階ですでにこれらの技法を開発者に対して提供している。JavaScriptの次期バージョンを待つ必要はない。TypeScriptのコードはメジャーなWebブラウザのすべてで有効なJavaScriptコードに変換される。
クラス宣言
クラス・ベースのプログラミングについて簡単な例を示そう:
class Greeter { greeting: string; constructor(message: string) { this.greeting = message; } greet() { return "Hello, " + this.greeting; } } var greeter = new Greeter("world");
構文はC#やJavaのそれとそっくりだ。Greeter
クラスは3つのメンバー─公開プロパティとコンストラクタ、そしてメソッド─を持っている。メンバーを参照するのにthis.
が必要になることに注意されたい。スコープ・チェーンの関係上、JavaScriptではこれは省略できない。
継承/拡張
もちろんクラスは他のクラスを継承し拡張できる。
class Animal { name:string; constructor(theName: string) { this.name = theName; } move(meters: number = 0) { alert(this.name + " moved " + meters + "m."); } } class Snake extends Animal { constructor(name: string) { super(name); } move(meters = 5) { alert("Slithering..."); super.move(meters); } } class Horse extends Animal { constructor(name: string) { super(name); } move(meters = 45) { alert("Galloping..."); super.move(meters); } } var sam = new Snake("Sammy the Python"); var tom: Animal = new Horse("Tommy the Palomino"); sam.move(); tom.move(34); ||< ** public/private修飾子 *** デフォルトpublic TypeScriptではメンバーの可視性はデフォルトで<code>public</code>だ。クラスの外側の世界からメンバーを隠したい場合には<code>private</code>を指定する。 >|typescript| class Animal { private name:string; constructor(theName: string) { this.name = theName; } move(meters: number) { alert(this.name + " moved " + meters + "m."); } }
privateメンバーの注意点
TypeScriptは構造的な型に基づくシステムである。2つの値の型が比較されるとき、それらがどのように生成されたかは関知されない。それぞれの型のメンバーに互換性があると判断されれば、型もまた互換性があると判断されることになる。
重要な点として、private
なメンバーを持つ型同士はいつでも異なる型と見なされる。private
なメンバーの名前も型のいずれもが一致していても、互換的とは見なされない。ある2つのオブジェクトがprivate
なメンバーを持っていて、にもかかわらず互換性があると判断されるとすれば、それはそのメンバーが同じ1つのクラス宣言─自クラスであれ継承したクラスであれ─で宣言されたものである場合だけである。
class Animal { private name:string; constructor(theName: string) { this.name = theName; } } class Rhino extends Animal { constructor() { super("Rhino"); } } class Employee { private name:string; constructor(theName: string) { this.name = theName; } } var animal = new Animal("Goat"); var rhino = new Rhino(); var employee = new Employee("Bob"); animal = rhino; animal = employee; //エラー: Animal と Employee は互換性がない
パラメータ・プロパティ
public
/private
キーワードはクラスのプロパティを宣言するショートハンドとして機能する。先に例として登場したクラスを書き換えてみよう:
class Animal { constructor(private name: string) { } move(meters: number) { alert(this.name + " moved " + meters + "m."); } }
アクセサ
TypeScriptではオブジェクト・メンバーへのアクセスをインターセプトするgetter/setterを定義する機能をサポートしている。この機能によりオブジェクトの個々のプロパティに対してどのようにアクセスが行われるべきか、細かな制御を行うことが可能になる。まずgetter/setterなしの例を示そう:
class Employee { fullName: string; } var employee = new Employee(); employee.fullName = "Bob Smith"; if (employee.fullName) { alert(employee.fullName); }
このオブジェクトのプロパティにはどんな値であろうと直接設定することができる。しかし好き勝手に値を書き換えられてしまったらトラブルの原因となる可能性がある。
新しいバージョンのコードではそれが定義された名前空間でのみプロパティの値を変更できるように変更されている。
var passcode = "secret passcode"; class Employee { private _fullName: string; get fullName(): string { return this._fullName; } set fullName(newName: string) { if (passcode && passcode == "secret passcode") { this._fullName = newName; } else { alert("Error: Unauthorized update of employee!"); } } } var employee = new Employee(); employee.fullName = "Bob Smith"; if (employee.fullName) { alert(employee.fullName); }
staticなプロパティ
これまでのところわれわれはインスタンス・メンバーについてのみ述べてきた。これらはインスタンス化してはじめてアクセスが可能になるメンバーである。ところでクラスにはstatic
なメンバーを宣言することもできる。このメンバーにはインスタンスではなくクラスを使ってアクセスすることができる。次の例ではorigin
という名前のstatic
プロパティが宣言されており、各インスタンスからその値にアクセスするときには前側にクラス名を伴わせていることがわかる。これはインスタンス・メンバーに対するthis.
と同じようなものだ。
高度な話題
コンストラクタ関数
インターフェースに関する説明のセクションでも触れたが、TypeScriptでクラスを宣言するとき、実のところ2つの型が同時に宣言されていることになる。型の1つはクラスのインスタンスの型だ。
class Greeter { greeting: string; constructor(message: string) { this.greeting = message; } greet() { return "Hello, " + this.greeting; } } var greeter: Greeter; greeter = new Greeter("world"); alert(greeter.greet());
この例の中でvar greeter: Greeter
という変数の宣言で用いられているGreeter
はGreeter
クラスのインスタンスが持つ型である。クラス・ベースのオブジェクト指向に親しみのある方にはわざわざ言うまでもないことだろう。
型のもう1つはコンストラクタ関数だ。これはnew
キーワードを使ってインスタンスを生成するときに呼び出される関数である。このことをよく理解するには先ほどのコードから生成されるJavaScriptコードを見るのがよい:
var Greeter = (function () { function Greeter(message) { this.greeting = message; } Greeter.prototype.greet = function () { return "Hello, " + this.greeting; }; return Greeter; })(); var greeter; greeter = new Greeter("world"); alert(greeter.greet());
このコードの中でvar Greeter
には関数が代入されている。new
キーワードを使用したときこの関数が呼び出され、われわれはクラスのインスタンスを得ることとなる。コンストラクタ関数はクラスのstatic
なメンバーを保持する役割も担っている。クラスについて考察する上でのもう1つの切り口はインスタンスの側とstatic
な側の違いだろう。
この違いを理解するために先ほどの例を少しばかり修正しよう:
class Greeter { static standardGreeting = "Hello, there"; greeting: string; greet() { if (this.greeting) { return "Hello, " + this.greeting; } else { return Greeter.standardGreeting; } } } var greeter1: Greeter; greeter1 = new Greeter(); alert(greeter1.greet()); var greeterMaker: typeof Greeter = Greeter; greeterMaker.standardGreeting = "Hey there!"; var greeter2:Greeter = new greeterMaker(); alert(greeter2.greet());
greeter1
についてはとくに説明は不要だろう。
一方、greeterMaker
は「クラスそれ自身」を参照している。typeof Greeter
というコードによって「Greeter
というクラスの型それ自身」が得られる。これは「Greeter
クラスのインスタンスの型」とは別物だ。もう少し正確に言い直すと「Greeter
というシンボルで指示される型」を得ているということだ。そしてそれはコンストラクタ関数の型である。だからgreeterMaker
にnew
を付けて呼びだすとGreeter
のインスタンスが得られる。
クラスをインターフェースとして利用する
クラス宣言は型をつくり出す。だから、それをインターフェースと同じ場所で使用することもできる:
class Point { x: number; y: number; } interface Point3d extends Point { z: number; } var point3d: Point3d = {x: 1, y: 2, z: 3};
* * *
原典はTypeScriptの公式サイトにあるhandbookページ(2016/03/16取得)。