TypeScriptのクラスは型の付いたJavaScriptのクラスです。各フィールドはクラスの本体で型付きで宣言し、メソッドとコンストラクターの引数と戻り値にも型を付けます。
コンパイル結果は型を取り除いた普通のJavaScriptのクラスなので、JavaScriptのクラス の内容はすべてそのまま当てはまります。TypeScriptが加えるのはコンパイル時の処理です。最後の呼び出しはプログラムを実行する前に拒否されます。
フィールドの宣言
コンストラクターで代入する前に、フィールドをクラスの本体で宣言する必要があります。owner を宣言せずに this.owner に代入するとエラー TS2339(プロパティが存在しない)になります。初期値のあるフィールドは、変数と同じく値から型が推論されます。
| 宣言 | 意味 |
|---|---|
name: string; | コンストラクターで代入しなければならない |
count = 0; | 初期化済み、型は number と推論される |
label?: string; | 省略可能、型は string | undefined |
readonly id: number; | 一度だけ代入し、その後は読み取り専用 |
data!: string[]; | 確実な代入: 必ず設定されると信じてもらう |
static total = 0; | インスタンスではなくクラスに属する |
strict が有効なら、strictPropertyInitialization が省略可能でないすべてのフィールドに値が入るかをチェックします。次のクラスは2つ忘れています:
コンパイラーは両方を報告します:
index.ts(3,5): error TS2564: Property 'name' has no initializer and is not definitely assigned in the constructor.
index.ts(4,5): error TS2564: Property 'age' has no initializer and is not definitely assigned in the constructor.
このチェックがなければ、new Profile().name.toUpperCase() は型チェックを通り、undefined でクラッシュします。各フィールドは次の4つのどれかで直します。初期値を付ける、コンストラクターで代入する、? で省略可能にする、フレームワークや init() メソッドがあとで設定するなら name!: string と書く。最後の方法はそのフィールドのチェックを止めてしまうので、控えめに使いましょう。
メソッドと this
メソッドは関数と同じように型付けします。メソッドの中の this はインスタンスの型です。メソッドが this を返すとメソッドチェーンができ、戻り値の型はサブクラスにも自動で追従します。
class QueryBuilder {
private parts: string[] = [];
where(condition: string): this {
this.parts.push(condition);
return this;
}
build(): string {
return this.parts.join(" AND ");
}
}
const sql = new QueryBuilder().where("age > 18").where("active = 1").build();
console.log(sql); // age > 18 AND active = 1
メソッドをオブジェクトから取り出すと this が失われます。JavaScriptの典型的なバグです。TypeScriptはデフォルトではこれを捕まえませんが、メソッドが this パラメーターを宣言していれば捕まえます。このパラメーターは出力からは消え、呼び出し箇所を守るだけです:
アロー関数のフィールドがうまくいくのは、インスタンスごとに this を閉じ込めた関数が作られるからです。その代わり、プロトタイプ上で共有される1つのメソッドではなく、インスタンスごとに1つの関数ができます。
getter と setter
get と set のアクセサーは、外からはプロパティのように見えます。setter のない getter は読み取り専用で、代入するとコンパイルエラー(TS2540)になります。
コンパイラーは代入を拒否し、チェックを回避しても、コンパイルされたファイルは strict モードで動く(コンパイラーが先頭に "use strict" を出力する)ので、実行時にも TypeError が投げられます。setter の中の RangeError は自分で書いた実行時のチェックです。number という型は値が数値であることしか表さず、範囲については何も知りません。
static メンバー
static のフィールドとメソッドはクラスそのものに属します。インスタンスではなくクラス名を通してアクセスします。static { } ブロックはクラスが定義されたときに一度だけ実行されます。
static メンバーはクラスの型パラメーターを使えません。class Box<T> の中の static empty: T はエラー TS2302 です。static フィールドは1つしかなく、Box<string> や Box<number> などすべてで共有されるからです。
インターフェースを実装する
implements は、インターフェースが要求するものをクラスがすべて持っているかのチェックをコンパイラーに頼みます。メンバーが足りないとエラー TS2420: Class 'X' incorrectly implements interface 'Y' になります。
クラスは複数のインターフェースを実装できます: class Doc implements Printable, Serializable。implements がしないことが2つあります。クラスのメソッドの引数に型を付けてはくれません。check(s: string): boolean を実装するクラスの中の check(s) { ... } は、やはりエラー TS7006(Parameter 's' implicitly has an 'any' type)になるので、型注釈を書きましょう。また、実行時には何も残らないので、obj instanceof Shape はできません。
コンストラクターの中の private radius はパラメータープロパティで、フィールドの宣言と代入を一度に行います。コンストラクター を参照してください。
クラスは型でもある
クラス宣言は1つの名前で2つのものを作ります。値(new で呼び出すコンストラクター関数)と型(インスタンスの形)です。TypeScriptは型を構造で比較するので、同じ public メンバーを持つオブジェクトなら、クラスで作られていなくてもその型に当てはまります。
普通のオブジェクトは型チェックを通りますが、実行時には Point ではありません。({ x: 6, y: 8 }) instanceof Point は false です。クラスに private や #private のメンバーがあればこれを防げます。そのクラス(またはサブクラス)のインスタンスだけが代入できるようになります。
よくある間違い
- フィールドを宣言して一度も代入しない。
TS2564は、フィールドがundefinedになると教えてくれています。!で黙らせるのではなく初期化しましょう。 - メソッドをコールバックとして渡す。
button.onclick = obj.handleではthisが失われます。アロー関数のフィールドかobj.handle.bind(obj)を使いましょう。 implementsがコードや型を追加すると期待する。 チェックするだけです。引数の型には型注釈が必要です。- インターフェースに
instanceofを使う。 インターフェースは実行時に存在しません。クラスをチェックするか、型ガードを使いましょう。 - 型がデータを検証すると思い込む。
numberのフィールドはコンパイル時にはどんな数値も受け付け、実行時には型のないデータから何でも入ってきます。
よくある質問
TypeScriptでクラスを作るには?
クラスの本体で各フィールドを型付きで宣言し、コンストラクターとメソッドはJavaScriptと同じように書きます: class User { name: string; constructor(name: string) { this.name = name; } greet(): string { return "Hi, " + this.name; } }。インスタンスは new User("Ada") で作ります。
「has no initializer and is not definitely assigned in the constructor」とはどういう意味ですか?
strictPropertyInitialization(strict の一部)によるエラー TS2564 です。string 型のフィールドを何も設定していないので、undefined から始まってしまいます。初期値を付ける(name = "")、コンストラクターで代入する、省略可能なフィールドにする(name?: string)、コンストラクターの外で設定されるなら確実な代入アサーション(name!: string)を使う、のいずれかで直します。
TypeScriptの implements と extends の違いは何ですか?
extends は親クラスのコードを継承します。親のフィールドとメソッドは実行時に子クラスに存在します。implements は、クラスがインターフェースの形を持っているかのチェックをコンパイラーに頼むだけです。クラスには何も追加されず、コンパイル後のJavaScriptからは消えます。
TypeScriptでクラスを型として使えますか?
使えます。クラス名は値(コンストラクター)でもあり型(インスタンスの形)でもあります。TypeScriptは型を構造で比較するので、let u: User は同じ public メンバーを持つどんなオブジェクトも受け付けます。typeof User はコンストラクターそのものの型です。
TypeScriptのクラスは実行時にJavaScriptのクラスと違いますか?
違いません。TypeScriptはクラスを普通のJavaScriptのクラスにコンパイルします。型注釈、implements 句、private のような修飾子は消去されます。実行時に存在するのは、#private フィールド、static ブロック、getter のようなJavaScriptの機能だけです。