HTML5テクニカルノート
TypeScript: クラス
- ID: FN1703001
- Technique: HTML5 / JavaScript
- Package: TypeScript 2.2
TypeScript公式Handbook「Classes」をもとにした解説です。ECMAScript 2015(ECMAScript 6)には構文にクラスが加わりました。TypeScriptも、この仕様に沿ってクラスを採り入れています。コンパイルされるJavaScriptコードは、ES6に対応する前のブラウザで動きます。
01 クラスを定める
クラスはclass
キーワードで定めます。つぎのクラス(Point)は、コンストラクタ(constructor()
)に渡したふたつの引数(xy座標)をプロパティに与えてインスタンスがつくられます。クラスの中でメンバー(プロパティとメソッド)を参照するには、this
を添えなければなりません。メソッド(getCoords())を呼び出せば、それらの値をプロパティとしてもつオブジェクトが得られます。なお、関数(function
)と異なり、定義する前に参照したり、コンストラクタを呼び出そうとするとエラー(ReferenceError)になります。
class Point { x: number; y: number; constructor(x: number, y: number) { this.x = x; this.y = y; } getCoords(): {x: number, y: number} { return {x: this.x, y: this.y}; } } let point: Point = new Point(3, 4); console.log(point.getCoords()); // { x: 3, y: 4 }
02 継承する
extends
キーワードを以下のように用いて、クラスは継承できます。子のクラス(サブクラス)は、親のクラス(スーバークラス)のメンバーを受け継ぎ、みずからのものとして扱えるのです(前出「TypeScript入門 03: クラスを継承して使う」参照)。サブクラスのコンストラクタは、本体から関数super()
でスーパークラスのコンストラクタを呼び出します。この呼び出しをする前に、this
参照はできません。
ふたつのサブクラスにはともに、スーパークラスのxy座標のプロパティ(xとy)、および原点から座標までの距離を返すメソッド(getLength())が継承されます。ひとつめのサブクラスVectorは、コンストラクタが受け取ったxy座標の引数を、そのままsuper()
関数でスーパークラスに渡しています。また、独自のメソッドgetAngle()は、Math.atan2()
メソッドにより原点とxy座標のなす角度を返します。
もうひとつのサブクラスPolarでは、原点からの距離と角度を引数としてインスタンスがつくられます。コンストラクタはスーパークラスのコンストラクタに渡す引数(xとy)の値を求め、受け取った引数から独自のプロパティ(radiusとangle)も定めています(距離と角度から座標を求める計算については「sinとcosは何する関数?」をご参照ください)。原点と座標との角度を返す独自のメソッドgetAngle()が加えられたほか、親クラスのメソッドgetLength()を定義し直しました(オーバーライド)。プロパティ値(radius)をそのまま返せば済むからです。
class Point { x: number; y: number; constructor(x: number, y: number) { this.x = x; this.y = y; } getLength(): number { let squaredSum = this.x * this.x + this.y * this.y; return Math.sqrt(squaredSum); } } class Vector extends Point { constructor(x: number, y: number) { super(x, y); } getAngle(): number { return Math.atan2(this.y, this.x); } } class Polar extends Point { radius: number; angle: number; constructor(radius: number, angle: number) { let x: number = radius * Math.cos(angle); let y: number = radius * Math.sin(angle); super(x, y); // this参照する前に呼び出す this.radius = radius; this.angle = angle; } getLength(): number { // スーパークラスのメソッドをオーバーライド return this.radius; } getAngle(): number { return this.angle; } }
ふたつのサブクラスをつぎのように試してみます。渡したxy座標はともに(1, √3)ですので、原点からの距離は2です。ひとつめのサブクラス(Vector)のインスタンス(vector)は、スーパークラスから継承したメソッド(getLength())で距離を得ました。計算上の誤差がわずかに生じています。ふたつめのサブクラス(Polar)のインスタンス(polar)は、オーバーライドしたメソッド(getLength())により、コンストラクタが受け取ったプロパティ値(radius)をそのまま返しているため誤差がありません。
let vector: Point = new Vector(1, Math.sqrt(3)); console.log(vector.getLength()); // 1.9999999999999998 let polar: Point = new Polar(2, Math.PI / 3); console.log(polar.getLength()); // 2
03 アクセス修飾子を使う
アクセス修飾子を使うと、SNSのようにクラスのメンバーの公開範囲が決められます(図001)。デフォルトはすべてに公開されるpublic
です。private
のメンバーは、そのクラスの中でしか参照できません。そのクラスと継承したサブクラスだけ扱えるのがprotected
です。
図001■アクセス修飾子は公開範囲を決める
03-01 デフォルトはpublic
クラスのメンバーにアクセス修飾子を添えないと、public
とみなされます。つぎのクラスでpublic
修飾子を省いても、同じ定義だということです。メンバーはすべてクラスの外から参照できます。
class Point { public x: number; public y: number; public constructor(x: number, y: number) { this.x = x; this.y = y; } public getCoords(): {x: number, y: number} { return {x: this.x, y: this.y}; } }
03-02 private修飾子でアクセス制限する
private
修飾子を添えたメンバーは、クラスの中からしか参照できません。サブクラスはスーパークラスのprivate
なメンバーを継承します。けれど、サブクラスであっても、アクセスはできないのです。つぎのように親クラス(Point)に備えたprivate
でないメソッド(getCoords())から値を得なければなりません。設定のメソッドがなければ、プロパティは読み取り専用になります。
class Point { private x: number; private y: number; constructor(x: number, y: number) { this.x = x; this.y = y; } getCoords(): {x: number, y: number} { return {x: this.x, y: this.y}; } } class Vector extends Point { constructor(x: number, y: number) { super(x, y); } getAngle(): number { let coords = this.getCoords(); // publicなメソッドは呼び出せる // return Math.atan2(this.y, this.x); // privateなのでクラスの外から参照できないというエラー return Math.atan2(coords.y, coords.x); } } let vector: Vector = new Vector(1, Math.sqrt(3)); console.log(vector.getAngle() / Math.PI * 180); // 60
TypeScriptでは、同じ名前のプロパティを同じ型でもっていれば、型は合っているとされるのが基です(「TypeScript入門 08: 型の互換性」02「プロパティとその型で互換性を調べる」参照)。ただし、メンバーがすべてpublic
の場合にかぎります。private
(あるいは後述するprotected
)のメンバーについては、属するクラスも同じでなければなりません。つまり、private
のメンバーをもつクラスで型づけすると、そのクラスかサブクラスのインスタンスしか合わないのです。
つぎのふたつのクラスPointとCoordsは、メンバーの名前と型づけ、およびprivate
修飾子までまったく同じです。けれど、private
なメンバーの属するクラスが異なるので型は合いません。サブクラスVectorのインスタンスは親のprivate
なメンバーを継承するため、型が合うとされるのです。
class Point { private x: number; private y: number; constructor(x: number, y: number) { this.x = x; this.y = y; } } class Vector extends Point { constructor(x: number, y: number) { super(x, y); } } class Coords { private x: number; private y: number; constructor(x: number, y: number) { this.x = x; this.y = y; } } let vector: Point = new Vector(3, 4); // let coords: Point = new Coords(3, 4); // privateプロパティの属するクラスが違うのでエラー
03-03 protected修飾子のメンバーをサブクラスから参照する
protected
修飾子を添えたメンバーは、定めたクラスとサブクラスのみ参照できます。つぎのクラスPointは、protected
のメソッド(getSquaredSum())がprivate
の座標のプロパティ(xとy)から、原点との差の平方和を返すことにしました。3次元座標のサブクラス(Vector3D)はz座標が加わるので、原点からの距離はオーバーライドしたメソッド(getLength())で求めます。protected
のメソッドから得たxy座標の平方和にz座標の2乗を加えた平方根が距離です[*1]。
class Point { private x: number; private y: number; constructor(x: number, y: number) { this.x = x; this.y = y; } protected getSquaredSum(): number { return this.x * this.x + this.y * this.y; } getLength(): number { return Math.sqrt(this.getSquaredSum()); } } class Vector3D extends Point { private z: number; constructor(x: number, y: number, z: number) { super(x, y); this.z = z; } getLength(): number { let squaredSum = this.getSquaredSum() + this.z * this.z; // protectedのメソッドをサブクラスから呼び出す return Math.sqrt(squaredSum); } }
試しに、3次元座標のサブクラス(Vector3D)で、つぎのような座標(√2/2, √2/2, √3)のインスタンス(vector3d)をつくりました。オーバーライドしたメソッド(getLength())は、スーパークラスのprotected
のメソッド(getSquaredSum())が返す値から、距離を求めています。protected
のメソッドを、外から直接インスタンスに対して呼び出せばエラーになります。
let vector3d: Vector3D = new Vector3D(Math.SQRT1_2, Math.SQRT1_2, Math.sqrt(3)); console.log(vector3d.getLength()); // 2 let point: Point = new Point(1, 1); // point.getSquaredSum(); // protectedのメソッドは外から呼び出せないのでエラー
protected
修飾子をコンストラクタ(constructor()
)に与えることもできます。そのクラスのインスタンスは、コンストラクタを外から呼び出してつくることができません。クラスの継承はできますので、サブクラスのコンストラクタから関数super()
で呼び出すことになります。
class Point { private x: number; private y: number; protected constructor(x: number, y: number) { this.x = x; this.y = y; } protected getCoords(): {x: number, y: number} { return {x: this.x, y: this.y}; } } class Vector extends Point { constructor(x: number, y: number) { super(x, y); // スーパークラスのコンストラクタを呼び出す } getAngle(): number { let coords = this.getCoords(); return Math.atan2(coords.y, coords.x); } } let vector: Vector = new Vector(1, Math.sqrt(3)); console.log(vector.getAngle() / Math.PI * 180); // 60 // let point: Point = new Point(0, 0) // protectedのコンストラクタは外から呼び出せないのでエラー
図002■3次元空間座標の原点からの距離
03-04 引数でプロパティを定める
コンストラクタが受け取った引数の値をそのままプロパティに与える場合には、引数でプロパティ宣言できます。そのためには、引数にアクセス修飾子(または後述のreadonly
修飾子)を添えるだけです。プロパティ宣言のステートメントや代入の式が省けるので、コードは短くなります。
class Point { constructor(private x: number, private y: number) {} } class Vector3D extends Point { constructor(x: number, y: number, private z: number) { super(x, y); } } let vector: Vector3D = new Vector3D(3, 4, 5); console.log(vector); // Vector3D { x: 3, y: 4, z: 5 }
04 静的なメンバーを定める
静的なメンバーは、インスタンスをつくらずに、クラスから参照します。添える修飾子はstatic
です。つぎのコードは、スーパークラス(Point)に静的プロパティ(radToDeg)、サブクラス(Vector)に静的メソッド(toDegree())を定めました。静的プロパティradToDegは、角度の単位ラジアンから度数への変換比率です。静的メソッドtoDegree()は、その比率を用いて、渡された引数のラジアン角を度数値に変換します。静的メソッドの本体からスーパークラスの静的メンバーを参照するには、クラスのほかにキーワードsuper
も使えます。
class Point { static radToDeg: number = 180 / Math.PI; protected constructor(private x: number, private y: number) {} protected getCoords(): {x: number, y: number} { return {x: this.x, y: this.y}; } } class Vector extends Point { constructor(x: number, y: number) { super(x, y); } static toDegree(angle: number): number { // 静的メソッド return angle * super.radToDeg; // スーパークラスの静的プロパティを参照 } getAngle(): number { let coords = this.getCoords(); return Math.atan2(coords.y, coords.x); } } let vector: Vector = new Vector(1, Math.sqrt(3)); console.log(vector.getAngle() * Point.radToDeg); // 59.99999999999999 console.log(Vector.toDegree(Math.PI)); // 180
05 readonly修飾子で読み取り専用に定める
プロパティにreadonly
修飾子を添えると、読み取り専用になります(「TypeScript 2.0のreadonly修飾子」参照)。つぎのように、静的なプロパティ(RAD_TO_DEG)を値の変えられないクラス定数にすることもできるのです。
class Point { static readonly RAD_TO_DEG: number = 180 / Math.PI; protected constructor(private x: number, private y: number) {} protected getCoords(): {x: number, y: number} { return {x: this.x, y: this.y}; } } class Vector extends Point { constructor(x: number, y: number) { super(x, y); } getAngle(): number { let coords = this.getCoords(); return Math.atan2(coords.y, coords.x); } } let vector: Vector = new Vector(1, Math.sqrt(3)); console.log(vector.getAngle() * Point.RAD_TO_DEG); // 59.99999999999999 // Point.RAD_TO_DEG = 0; // 読み取り専用なので書き替えられないというエラー
readonly
プロパティは、コンストラクタからであれば値が改められます。つぎのコードは、クラス(Item)のprivate
な静的プロパティ(nextId)と組み合わせて、コンストラクタでreadonly
のプロパティ(id)に整数連番を与えます。
class Item { private static nextId: number = 0; readonly id: number = 0; constructor() { this.id = Item.nextId++; } } let item0: Item = new Item(); let item1: Item = new Item(); console.log(item0.id, item1.id); // 0 1 // item0.id = 2; // 読み取り専用なので書き替えられないというエラー
06 アクセサでプロパティのようにメソッドを扱う
TypeScriptは、get
/set
アクセサでメソッドをプロパティのように扱えます(「TypeScript入門 05: get/setアクセサを使う」参照)。メソッド名の前にget
またはset
のキーワードを添えれば、その名前をプロパティのようにして、値の取り出しや書き替えができるのです。
つぎのコードでは、スーパークラス(Point)が座標のprivate
なプロパティ(xとy)をもち、メソッドでそれらの座標の取得・設定(getCoords()とsetCoords())や原点からの距離(getLength())も得られるのです。get
/set
アクセサはサブクラス(Vector)に定め、座標が原点となすラジアン角をプロパティ(angle)のかたちで、取得・設定できるようにしました。プロパティそのものはありませんので、アクセサが参照されるたびに値は計算されます。
class Point { static readonly RAD_TO_DEG: number = 180 / Math.PI; protected constructor(private x: number, private y: number) {} protected getCoords(): {x: number, y: number} { return {x: this.x, y: this.y}; } protected setCoords(x: number, y: number): void { this.x = x; this.y = y; } getLength(): number { let squaredSum = this.x * this.x + this.y * this.y; return Math.sqrt(squaredSum); } } class Vector extends Point { constructor(x: number, y: number) { super(x, y); } get angle(): number { let coords = this.getCoords(); return Math.atan2(coords.y, coords.x); } set angle(angle: number) { let radius: number = this.getLength(); let x: number = radius * Math.cos(angle); let y: number = radius * Math.sin(angle); this.setCoords(x, y); } } let vector: Vector = new Vector(Math.SQRT2, Math.SQRT2); console.log(vector.angle * Point.RAD_TO_DEG); // 45 vector.angle = Math.PI / 3; console.log(vector); // Vector { x: 1.0000000000000002, y: 1.7320508075688772 }
readonly
修飾子で定めたプロパティを、get
アクセサで書き替えることもできます。set
アクセサが加えられていなければ、読み取り専用になるからです。
// static readonly RAD_TO_DEG: number = 180 / Math.PI; static get RAD_TO_DEG() { return 180 / Math.PI; }
readonly
修飾子でもget
アクセサでも、同じように定数が定められます。けれど、コンパイルされるJavaScriptコードの実装は違います。readonly
修飾子の場合、JavaScriptには読み取り専用の修飾子がないので、プロパティはただクラスに加えられるだけです。そのため、実行時に値が書き替えられます(図003)。
Point.RAD_TO_DEG = 180 / Math.PI;
図003■readonly修飾子を与えたプロパティは実行時に書き替えできる
ECMAScript 5.1からget
構文が備わりました。get
アクセサはこの構文にコンパイルされるため、返す値は実行時も書き替えできません(図004)。なお、get
/set
アクセサはECMAScript 5より前の設定では使えないことにご注意ください。
図004■getアクセサの返す値は実行時に書き替えできない
コンパイルされるJavaScriptコードは、ECMAScript 5とECMAScript 2015とではつぎのように異なります。
// ECMAScript 5 Object.defineProperty(Point, "RAD_TO_DEG", { get: function () { return 180 / Math.PI; }, enumerable: true, configurable: true });
// ECMAScript 2015 class Point { static get RAD_TO_DEG() { return 180 / Math.PI; } }
07 抽象クラスを継承する
抽象クラスは、サブクラスで継承する基本クラス(スーパークラス)です。クラスの前にabstract
修飾子を添えます。また、抽象クラスのメンバーにabstract
修飾子を与えれば、インタフェース(interface
)と同じように、サブクラスは同じ名前で同じ型づけのメンバーを備えなければなりません。abstract
修飾子を添えたメンバーには、インタフェースと同じく、実装は書きません。
abstract class Point { constructor(private x: number, private y: number) {} getCoords(): {x: number, y: number} { return {x: this.x, y: this.y}; } abstract getLength(): number; abstract getAngle(): number; } class Vector extends Point { constructor(x: number, y: number) { super(x, y); } getLength(): number { let coords = this.getCoords(); let square: number = coords.x * coords.x + coords.y * coords.y return Math.sqrt(square); } getAngle(): number { let coords = this.getCoords(); return Math.atan2(coords.y, coords.x); } } class Polar extends Point { private radius: number; private angle: number; constructor(radius: number, angle: number) { let x: number = radius * Math.cos(angle); let y: number = radius * Math.sin(angle); super(x, y); this.radius = radius; this.angle = angle; } getLength(): number { return this.radius; } getAngle(): number { return this.angle; } } // let point: Point = new Point(0, 0); // 抽象クラスのインスタンスはつくれないというエラー let vector: Point = new Vector(1, Math.sqrt(3)); console.log(vector.getLength()); // 1.9999999999999998 let polar: Point = new Polar(2, Math.PI / 3); console.log(polar.getAngle() * 180 / Math.PI); // 59.99999999999999
08 クラスの特別な使い方
08-01 クラスを納める型づけ
変数にクラスを型として与えると、クラスのインスタンスが代入できます。たとえば、以下のクラスPointが定められているとき、そのクラスで型づけした変数(point)にインスタンスを納めて、メソッドが呼び出せます。
let point: Point = new Point(1, Math.sqrt(3)); console.log(point.angle); // 1.0471975511965976
class Point { static readonly RAD_TO_DEG: number = 180 / Math.PI; static asDegree: boolean = false; constructor(private x: number, private y: number) {} get angle(): number { let angle: number = Math.atan2(this.y, this.x); if (Point.asDegree) { angle *= Point.RAD_TO_DEG; } return angle; } }
では、クラスそのものを変数に入れたいとき、どのように型を決めたらよいでしょう。その場合、キーワードtypeof
にクラスを添えて型づけします(「TypeScript早わかりチートシート【1.5.3対応】」の「型クエリ」参照)。すると、変数にクラス(コンストラクタ関数)がそっくり納められます。その変数をクラスの参照としてインスタンスがつくれますし、静的なプロパティの値もつぎのように書き替えられます。
let point: Point; point = new Point(1, Math.sqrt(3)); console.log(point.angle); // 1.0471975511965976 let pointClass: typeof Point = Point; pointClass.asDegree = true; console.log(point.angle); // 59.99999999999999
08-02 クラスでインタフェースを拡張する
クラスはインタフェースが継承することもできます。継承するのはメンバーの名前と型づけだけで、実装は含まれません。インタフェース(IVector3D)を実装(implements
)したサブクラス(Vector3D)は、改めて継承されたクラス(Point)のメンバーを実装しなければならないのです。なお、インタフェースが継承したクラスにpublic
でないメンバーを含む場合については、「TypeScript: インタフェース」10「クラスでインタフェースを拡張する」をご参照ください。
class Point { constructor(public x: number, public y: number) {} getLength(): number { let square: number = this.x * this.x + this.y * this.y; return Math.sqrt(square); } } interface IVector3D extends Point { z: number; } class Vector3D implements IVector3D { constructor(public x: number, public y: number, public z: number) {} getLength(): number { // 実装しないとエラー let squaredSum = this.x * this.x + this.y * this.y + this.z * this.z; return Math.sqrt(squaredSum); } } let vector3d: IVector3D = new Vector3D(Math.SQRT1_2, Math.SQRT1_2, Math.sqrt(3)); console.log(vector3d.getLength()); // 2
インタフェースが継承したクラスを、別にスーパークラスとして拡張することもできます。その場合には、親クラスの実装が継承されます。
class Vector3D extends Point implements IVector3D { constructor(x: number, y: number, public z: number) { super(x, y); } getLength(): number { // スーバークラスのメソッドをオーバーライド let squaredSum = super.getLength() + this.z * this.z; // スーバークラスのメソッドを呼び出して使う return Math.sqrt(squaredSum); } }
作成者: 野中文雄
作成日: 2017年3月12日
Copyright © 2001-2017 Fumio Nonaka. All rights reserved.