サイトトップ

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

HTML5テクニカルノート

TypeScript入門 04: オブジェクト型リテラルとインタフェースを使う


TypeScriptでは、変数や関数の引数、戻り値、プロパティあるいはメソッドに型づけできます。その型は予め備わっているデータだけでなく、独自に定めることもできます。型定義に用いられるオブジェクト型リテラルとインタフェースについてご説明します。

01 静的メソッドを分ける

本稿は、「TypeScriptTypeScript入門 03: クラスを継承して使う」で書いたコード003「サブクラスに静的メソッドを加えた」に手を加えてゆきます。メソッドの中に使い回せる処理があると、それを分けることがよくあります。スーパークラス(Point)の静的メソッド(polar())は、距離と角度からその座標がプロパティに納められたインスタンスを返します。この中の座標計算を、つぎのように別の静的メソッド(getPolar())に分けました。座標値はふたつ(xとy)あるので、オブジェクトリテラルで返します。したがって、戻り値はObjectで型づけしています。


class Point {

	static polar(length: number, angle: number): Point {
		/*
		var x: number = length * Math.cos(angle);
		var y: number = length * Math.sin(angle);
		return new Point(x, y);
		*/
		var point: Object = Point.getPolar(length, angle);  // ビルドするとエラー
		return new Point(point.x, point.y);
	}
	static getPolar(length: number, angle: number): Object {
		var x: number = length * Math.cos(angle);
		var y: number = length * Math.sin(angle);
		return {x: x, y: y};
	}
}

ところが、ビルドするとつぎのようなエラーが示されます。Objectクラスの中にはそのような(xとyという)プロパティがないからです。

Property 'x' does not exist on type 'Object'.
Property 'y' does not exist on type 'Object'.

切り分けた静的メソッド(getPolar())の戻り値をObjectで型づけされた変数(point)に納めようとするとき、互いの型が適合するかどうか確かめられます。変数の型としてanyを与えれば、この確認はされません。サブクラス(Vector)も併せてつぎのように型づけすれば、メソッドは正しく動きます。


class Point {

	static polar(length: number, angle: number): Point {
		// var point: Object = Point.getPolar(length, angle);
		var point: any = Point.getPolar(length, angle);
		return new Point(point.x, point.y);
	}
	static getPolar(length: number, angle: number): Object {
		var x: number = length * Math.cos(angle);
		var y: number = length * Math.sin(angle);
		return {x: x, y: y};
	}
}
class Vector extends Point {

	static polar(length: number, angle: number): Vector {
		// var point: Point = Point.polar(length, angle);
		var point: any = Point.getPolar(length, angle);
		return new Vector(point.x, point.y);
	}
}

02 型定義をつくる

anyで型づけすると、型が確認されません。型を与える意味が失われるということです。TypeScriptでは組み込みのデータ型になくても、型定義をつくることができます。「オブジェクト型リテラル」と呼ばれる書き方で、つぎのようにオブジェクトリテラルと同じかたちで、値の替わりに型を定めます(なお、TypeScript プログラミング「オブジェクト型リテラル」参照)


class Point {

	static polar(length: number, angle: number): Point {
		// var point: any = Point.getPolar(length, angle);
		var point: {x: number, y: number} = Point.getPolar(length, angle);
		return new Point(point.x, point.y);
	}
	// static getPolar(length: number, angle: number): Object {
	static getPolar(length: number, angle: number): {x: number, y: number} {
		var x: number = length * Math.cos(angle);
		var y: number = length * Math.sin(angle);
		return {x: x, y: y};
	}
}

この型づけであれば、与えられたプロパティがなかったり、そのデータ型が違っていれば、ビルドのときエラーが示されます。なお、型に含まれていないプロパティやメソッドがあっても、それはエラーになりません。ここまでのTypeScriptコードをつぎにまとめました(コード001)。

コード001■定義した型を静的メソッドの戻り値に定める


class Point {
	static RAD_TO_DEG: number = 180 / Math.PI;
	constructor(public x: number, public y: number) {
	}
	getLength(): number {
		var square: number = this.x * this.x + this.y * this.y;
		return Math.sqrt(square);
	}
	getAngle(): number {
		return Math.atan2(this.y, this.x);
	}
	static polar(length: number, angle: number): Point {
		var point: {x: number, y: number} = Point.getPolar(length, angle);
		return new Point(point.x, point.y);
	}
	static getPolar(length: number, angle: number): {x: number, y: number} {
		var x: number = length * Math.cos(angle);
		var y: number = length * Math.sin(angle);
		return {x: x, y: y};
	}
}
class Vector extends Point {
	constructor(x: number, y: number) {
		super(x, y);
	}
	scale(scale): void {
		this.x *= scale;
		this.y *= scale;
	}
	add(point: Point): void {
		this.x += point.x;
		this.y += point.y;
	}
	static polar(length: number, angle: number): Vector {
		var point: {x: number, y: number} = Point.getPolar(length, angle);
		return new Vector(point.x, point.y);
	}
}

03 インタフェースを定める

オブジェクト型リテラルで定めた同じ型を、あちこちで使うようになったら、少し考えなければなりません。プロパティが加わったり、データ型が変わると、それらすべてを直さなければならなくなるからです。そのような場合に備えるには、インタフェースを用います。つぎのようにキーワードinterfaceでクラスと同じように名前を与え、プロパティやメソッドの型だけを宣言します。


interface IPoint {
	x: number;
	y: number;
}

インタフェースを定めれば、つぎのようにその名前の参照で型づけできます。前述の追加や変更も、インタフェース定義を書き替えるだけです。


class Point {

	static polar(length: number, angle: number): Point {
		var point: IPoint = Point.getPolar(length, angle);
		return new Point(point.x, point.y);
	}
	static getPolar(length: number, angle: number): IPoint {
		var x: number = length * Math.cos(angle);
		var y: number = length * Math.sin(angle);
		return {x: x, y: y};
	}
}
class Vector extends Point {

	static polar(length: number, angle: number): Vector {
		var point: IPoint = Point.getPolar(length, angle);
		return new Vector(point.x, point.y);
	}
}

さらに、インタフェースはつぎのようにキーワードimplementsを用いて、クラス(Point)に定めることもできます。その場合、クラスはインタフェース(IPoint)が決めたプロパティをそのデータ型で備えなければなりません。また、インタフェースをメソッド(add())の引数の型に与えることもできます。型をクラスで定めると、そのクラスとサブクラスのインスタンス以外を渡せばエラーになります。インタフェースなら、プロパティとその型が合ってさえいれば、継承は問われません。


class Point implements IPoint {

	constructor(public x: number, public y: number) {
	}

}
class Vector extends Point {

	add(point: IPoint): void {
		this.x += point.x;
		this.y += point.y;
	}

}

たとえば、以下のテスト用のコードは、呼び出すメソッド(add())の引数にオブジェクトリテラルを渡しています。メソッドの引数がクラス(Point)で型づけされていたら、クラスのインスタンスではないため、つぎのようなエラーになります。インタフェース(IPoint)で与えていれば、プロパティと型が合っていれば動くのです。

Argument of type '{ x: number; y: number; }' is not assignable to parameter of type 'Point'.

var obj: Vector = Vector.polar(1, 0);
obj.add({x: 0, y: Math.sqrt(3)});  // インタフェースに適合する
console.log(obj.getLength(), obj.getAngle() * Point.RAD_TO_DEG);  // 1.9999999999999998 59.99999999999999

04 protected宣言によるアクセス制限

スーパークラスのメソッドをサブクラスでも使いたい場合、private宣言はできません。けれど、publicでは、つぎのようにどこからでもクラス(Point)のメソッド(getPolar())が呼び出せてしまいます。


console.log(Point.getPolar(Math.SQRT2, Math.PI / 4))  // (1, 1)

以下のようにクラス(Point)のメソッド(getPolar())に宣言protectedを加えると、そのクラスとサブクラスからしか参照できなくなります。外から呼び出そうとすれば、つぎのようなエラーが示されます。ここまで書いたTypeScriptコードは以下にまとめました(コード002)。なお、以下にサンプル001としてjsdo.itに書いたコードを掲げてあります。

Property 'getPolar' is protected and only accessible within class 'Point' and its subclasses.

class Point implements IPoint {

	protected static getPolar(length: number, angle: number): IPoint {
		var x: number = length * Math.cos(angle);
		var y: number = length * Math.sin(angle);
		return {x: x, y: y};
	}
}

コード002■オブジェクト型リテラルとインタフェースを使ったクラス定義


class Point implements IPoint {
	static RAD_TO_DEG: number = 180 / Math.PI;
	constructor(public x: number, public y: number) {
	}
	getLength(): number {
		var square: number = this.x * this.x + this.y * this.y;
		return Math.sqrt(square);
	}
	getAngle(): number {
		return Math.atan2(this.y, this.x);
	}
	static polar(length: number, angle: number): Point {
		var point: IPoint = Point.getPolar(length, angle);
		return new Point(point.x, point.y);
	}
	protected static getPolar(length: number, angle: number): IPoint {
		var x: number = length * Math.cos(angle);
		var y: number = length * Math.sin(angle);
		return {x: x, y: y};
	}
}
class Vector extends Point {
	constructor(x: number, y: number) {
		super(x, y);
	}
	scale(scale): void {
		this.x *= scale;
		this.y *= scale;
	}
	add(point: IPoint): void {
		this.x += point.x;
		this.y += point.y;
	}
	static polar(length: number, angle: number): Vector {
		var point: IPoint = Point.getPolar(length, angle);
		return new Vector(point.x, point.y);
	}
}
interface IPoint {
	x: number;
	y: number;
}

サンプル001■TypeScript: Using an interface to a class and data typing


作成者: 野中文雄
更新日: 2016年9月23日 TypeScriptのバージョンを2.0.3に更新。
作成日: 2016年9月12日


Copyright © 2001-2017 Fumio Nonaka.  All rights reserved.