サイトトップ

Director Flash 書籍 業務内容 プロフィール

HTML5テクニカルノート

TypeScript: クラス


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■アクセス修飾子は公開範囲を決める

図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のコンストラクタは外から呼び出せないのでエラー

[*1] 3次元空間の座標(x, y, z)の原点からの距離は、各座標の平方和を開いて得られます(図002)。。

図002■3次元空間座標の原点からの距離

図002

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修飾子を与えたプロパティは実行時に書き替えできる

図003

ECMAScript 5.1からget構文が備わりました。getアクセサはこの構文にコンパイルされるため、返す値は実行時も書き替えできません(図004)。なお、get/setアクセサはECMAScript 5より前の設定では使えないことにご注意ください。

図004■getアクセサの返す値は実行時に書き替えできない

図004

コンパイルされる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.