サイトトップ

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

HTML5テクニカルノート

TypeScript: インタフェース


TypeScriptは、データをインタフェースで型づけできます。TypeScriptで型が合うかどうかは、定められた型のプロパティ(メンバー)を照らし合わせて決めます。同じ名前のプロパティを同じ型でもっていれば、型は合っているとされるのです(「TypeScript入門 08: 型の互換性」参照)。

01 オブジェクト型リテラルとインタフェース

インタフェースを使わない場合、オブジェクト型リテラルで型づけすることができます(「オブジェクト型リテラルとインタフェースを使う」02「型定義をつくる」参照)。つぎの関数(getPointArray())は、引数をオブジェクト型リテラルで定めました。引数は、決められた名前のプロパティをその型でもっていればよく、それ以外のプロパティはとくに調べられません。


function getPointArray(point: {x: number, y: number}): number[] {
	let pointArray = [point.x, point.y];
	return pointArray;
}
let point3D = {x: 3, y: 4, z: 5};
let pointArray = getPointArray(point3D);
console.log(pointArray);  // [ 3, 4 ]

インタフェース(interface)は、クラスと似たかたちで、プロパティ名とその型を定めます。そして、つぎのように引数などのデータを、インタフェースで型づけられるのです。型が合うかどうかは、オブジェクト型リテラルの場合と同じく、同じ名前のプロパティを同じ型でもっているかどうかで確かめます[*1]。インタフェースの実装やクラスの継承を求めません(前出「TypeScript入門 08: 型の互換性」参照)。


interface IPoint {
	x: number;
	y: number;
}
function getPointArray(point: IPoint): number[] {
	let pointArray = [point.x, point.y];
	return pointArray;
}
let point3D = {x: 3, y: 4, z: 5};
let pointArray = getPointArray(point3D);

[*1] オブジェクトを直に関数に渡すと、プロパティが合わないというエラーになります。なお、後述04「余分なプロパティの扱い」をご参照ください。

interface IPoint {
	x: number;
	y: number;
}
function getPointArray(point: IPoint): number[] {

}
let pointArray = getPointArray({x: 3, y: 4, z: 5});  // エラー

02 省けるプロパティを定める

インタフェースに定めるプロパティを、省けるようにすることもできます。プロパティ名の後に?をつければ、そのプロパティはなくても構いません。たとえばつぎのように、省かれたプロパティにデフォルト値を与える使い方があります。


interface IPoint {
	x?: number;
	y?: number;
}
function getPointArray(point: IPoint): number[] {
	let pointArray = [0, 0];
	if (point.x) {
		pointArray[0] = point.x;
	}
	if (point.y) {
		pointArray[1] = point.y;
	}
	return pointArray;
}
let point3D = {y: 4, z: 5};
let pointArray = getPointArray(point3D);
console.log(pointArray);  // [ 0, 4 ]

プロパティは省けても、それがあるかどうかは確かめられます。ですから、コード補完には含まれるはずです(図001)。また、綴りを誤れば、エラーになります。

図001■省けるプロパティもコード補完に含まれる

図001

03 読み取り専用プロパティ

クラスやインタフェースのプロパティは、名前の前にreadonly修飾子を添えて読み取り専用に定められます。読み取り専用のプロパティにひとたび値を与えると、その後書き替えはできません。


interface IPoint {
	readonly x: number;
	readonly y: number;
}
let point: IPoint = {x: 0, y: 1};
point.x = 5;  // エラー

また、配列を読み取り専用にする型づけとして、ReadonlyArrayが備わっています(「TypeScript 2.0のreadonly修飾子」参照)。ジェネリックの書き方で、山かっこ<>の中に要素の型を与えます。この型づけをした変数に配列を納めると、値が書き替えられません。読み取り専用でない変数で同じ配列を参照すると書き替えができてしまうことについては、前出「TypeScript 2.0のreadonly修飾子」02「TypeScript 2.0のreadonly修飾子で定数を定める」をご参照ください。


let array: ReadonlyArray<number> = [0, 1, 2];
array[2] = 5;  // エラー
array.push(5);  // エラー
array.length = 2;  // エラー
let temp: number[] = array;  // エラー

04 余分なプロパティの扱い

インタフェースで型づけされた引数を定める関数に、インタフェースにないプロパティが含まれたオブジェクトを渡すとエラーになります(前述注[*1])。01「オブジェクト型リテラルとインタフェース」で示した例のように、予め変数に納めておくのが避け方のひとつです。もうひとつ、引数のオブジェクトを型変換するやり方もあります。


interface IPoint {
	x: number;
	y: number;
}
function getPointArray(point: IPoint): number[] {
	let pointArray = [point.x, point.y];
	return pointArray;
}
// let pointArray = getPointArray({x: 3, y: 4, z: 5});  // エラー
let pointArray = getPointArray({x: 3, y: 4, z: 5} as IPoint);
console.log(pointArray);  // [ 3, 4 ]

もっとも、関数がインタフェースに定められたプロパティしか用いないというのであれば、余分なプロパティがあっても構わないはずです。そういうとき、インタフェースをつぎのように定めると、ほかにプロパティがあっても決められた型(number)でありさえすれば、エラーが起こらなくなります。


interface IPoint {
	x: number;
	y: number;
	[propName: string]: number;
}
function getPointArray(point: IPoint): number[] {
	let pointArray = [point.x, point.y];
	return pointArray;
}
// let pointArray = getPointArray({x: 3, y: 4, z: '5'});  // 余分なプロパティが文字列なのでエラー
let pointArray = getPointArray({x: 3, y: 4, z: 5, w: 1});
console.log(pointArray);  // [ 3, 4 ]

05 関数の型づけ

インタフェースで、関数を型づけすることもできます。その場合は、つぎのように引数と戻り値の型を定めます。引数の名前は、関数が引き継がなくて構いません。


interface IPoint {
	x: number;
	y: number;
}
interface I2d {
	(element0: number, element1: number): IPoint;
}

関数のインタフェースで型づけした変数には、名前のない関数をインタフェースの型づけにしたがって与えます。関数の引数や戻り値に型づけを添えなくても、型推論によりインタフェースにもとづいて型が確かめられます(「TypeScript入門 08: 型の互換性」03「型推論」参照)。


interface IPoint {
	x: number;
	y: number;
}
interface I2d {
	(element0: number, element1: number): IPoint;
}
let point: I2d = function(x: number, y: number): IPoint {
	return {x: x, y: y};
}
let polar: I2d = function(radius, angle) {
	let x: number = radius * Math.cos(angle);
	let y: number = radius * Math.sin(angle);
	// return [x, y];  // 戻り値の型が合わないのでエラー
	return {x: x, y: y};
}
console.log(point(1, Math.sqrt(3)));  // { x: 1, y: 1.7320508075688772 }
console.log(polar(2, Math.PI / 3));  // { x: 1.0000000000000002, y: 1.7320508075688772 }
// polar(1, '√3');  // 第2引数が文字列なのでエラー

06 インデックス化できる型

配列や連想配列のように、インデックス(キー)に要素が納められるオブジェクトにも型づけできます。インタフェースには、角かっこ[]でインデックスの型、続くコロン:の後に要素の型を定めます。インデックスの型に使えるのは、文字列(string)か数値(number)です。関数の引数の型づけと同じく、インデックスの名前はとくに使われません。


interface StringArray {
	[index: number]: string;
}
let myArray: StringArray;
myArray = ['Bob', 'Fred'];
// myArray.length;  // 配列で型づけされてはいないのでエラー
let myStr: string = myArray[0];
console.log(myStr);  // Bob
console.log(myArray);  // [ 'Bob', 'Fred' ]
console.log((myArray as string[]).length);  // 2

インタフェースに、文字列インデックスと数値インデックスの要素をともに型づけすることもできます。その場合、数値インデックスに与える要素の型は、文字列インデックスの要素の型と合わなければなりません。JavaScriptは数値インデックスを文字列に変換して、オブジェクトのプロパティとするからです。文字列インデックスの要素をオブジェクトで型づけしたときは、数値インデックスの要素はそのサブクラスの型にしなければなりません。


class Point {
	x: number;
	y: number;
}
class Point3D extends Point {
	z: number;
}
interface IPoint {
	[coordinate: string]: Point;
	[Coordinate: number]: Point3D;
}
/* interface IPoint {
	[coordinate: string]: Point3D;
	[Coordinate: number]: Point;  // スーパークラスの型なのでエラー
} */
let coordinates: IPoint = {
	coord: {x: 1, y: 1}
};
coordinates[0] = {x: 1, y: Math.sqrt(3), z: 0};
console.log(coordinates);
// { '0': { x: 1, y: 1.7320508075688772, z: 0 }, coord: { x: 1, y: 1 } }
  

インデックスの要素を型づけしたインタフェースに、他のプロパティの型を加えることもできます。ただし、文字列のインデックス(キー)に値を納めるのは、JavaScriptがオブジェクトを定める基本のかたちです。したがって、加えたプロパティの型は、文字列インデックスの要素の型に合わなければなりません。また、インデックスで型づけした値は、角かっこ[]で参照します。


interface NumberDictionary {
	[index: string]: number;
	length: number;
	// name: string;  // 文字列インデックスの要素型に合わないのでエラー
}
let dict: NumberDictionary = {
	chihiro: 10,
	theta: 13,
	length: 2
}
console.log(dict);  // { chihiro: 10, theta: 13, length: 2 }
// console.log(dict.theta);  // インデックスを使わないのでエラー
console.log(dict['theta']);  // 13
console.log(dict.length);  // 2

数値インデックスの要素型は、インデックスが数値の場合の定めとなり、オブジェクトのプロパティすべてを決める結果にはなりません。


interface StringArray {
	[index: number]: string;
	length: number;
}
let myArray: StringArray = ['Bob', 'Fred'];
myArray.length = 2;
console.log(myArray);  // [ 'Bob', 'Fred' ]
console.log(myArray.length);  // 2

インデックスで定める要素の型に、readonly修飾子を与えることもできます。ひとたび値を納めたら、後から値を書き替えることも、要素を加えることもできません。


interface ReadonlyStringArray {
	readonly [index: number]: string;
}
let myArray: ReadonlyStringArray = ['Bob', 'Fred'];
myArray[0] = 'Alice';  // 値の書き替えはできないのでエラー
myArray[2] = 'Mallory';  // 要素の追加もできないのでエラー

07 クラスの型づけ

インタフェースで、クラスのプロパティやメソッドを型づけることもできます。参照を制限するprivateやクラスに静的に定めるstaticといった修飾子は加えられません。インスタンスのpublicとされるプロパティとメソッドの型づけです。


interface ICoordinates {
	x: number;
	y: number;
}
interface IPoint {
	coordinates: ICoordinates;
	setXY(x: number, y: number): void;
}
class Point implements IPoint {
	coordinates: ICoordinates;
	constructor(x: number, y: number) {
		this.coordinates = {x: x, y: y};
	}
	setXY(x: number, y: number): void {
		this.coordinates.x = x;
		this.coordinates.y = y;
	}
}
let point = new Point(1, 2);
point.setXY(3, 4);
console.log(point.coordinates);  // { x: 3, y: 4 }

インタフェースで、コンストラクタの型も定められます。


interface PointConstructor {
	new (cord0: number, cord1: number): IPoint;
}

ただし、クラスの型づけに用いることができません。コンストラクタは静的な定めだからです。


interface IPointConstructor {
	new (cord0: number, cord1: number): IPoint;
}
interface ICoordinates {
	x: number;
	y: number;
}
interface IPoint {
	coordinates: ICoordinates;
}
class Point implements IPointConstructor {  // コンストラクタのインタフェースをクラスに実装するとエラー
	coordinates: ICoordinates;
	constructor(x: number, y: number) {
		this.coordinates = {x: x, y: y};
	}
}

コンストラクタのインタフェースで型づけした変数にクラスの参照を納めれば、コンストラクタの引数や戻り値の型が確かめられるようになります。


interface IPointConstructor {
	new (cord0: number, cord1: number): IPoint;
}
interface ICoordinates {
	x: number;
	y: number;
}
interface IPoint {
	coordinates: ICoordinates;
}
class Point implements IPoint {
	coordinates: ICoordinates;
	constructor(x: number, y: number) {
		this.coordinates = {x: x, y: y};
	}
}
let _constuctor: IPointConstructor = Point;
let point: IPoint = new _constuctor(3, 4);
console.log(point);  // Point { coordinates: { x: 3, y: 4 } }

08 インタフェースの拡張

インタフェースは他のインタフェースを、クラスと同じようにextendsキーワードで拡張できます。目的に応じて組み合わせたインタフェースを、クラスへの実装などに使えるということです。


interface IPoint {
	setXY(x: number, y: number): void;
}
interface ICoordsXY extends IPoint {
	coordinates: {x: number, y: number};
}
class Point implements ICoordsXY {
	coordinates: {x: number, y: number};
	constructor(x: number, y: number) {
		this.coordinates = {x: x, y: y};
	}
	setXY(x: number, y: number): void {
		this.coordinates.x = x;
		this.coordinates.y = y;
	}
}
let point: Point = new Point(1, 2);
point.setXY(3, 4);
console.log(point.coordinates);  // { x: 3, y: 4 }

クラスとは異なり、インタフェースは複数拡張できます。実装するクラスに応じた組み合わせのインタフェースが定められるのです。


interface IPoint {
	setXY(x: number, y: number): void;
}
interface IPolar {
	setPolar(radius: number, angle: number): void;
}
interface ICoordsPolar extends IPolar, IPoint {
	coordinates: {radius: number, angle: number};
}

このインタフェースを実装した例が、以下のコード001です。つぎのように試すことができます。


let polar: Polar = new Polar(0, 0);
polar.setXY(1, Math.sqrt(3));
console.log(polar.coordinates.radius, polar.coordinates.angle * 180 / Math.PI);
// 1.9999999999999998 59.99999999999999

コード001■複数のインタフェースを拡張する


interface IPoint {
	setXY(x: number, y: number): void;
}
interface IPolar {
	setPolar(radius: number, angle: number): void;
}
interface ICoordsPolar extends IPolar, IPoint {
	coordinates: {radius: number, angle: number};
}
class Polar implements ICoordsPolar {
	coordinates: {radius: number, angle: number};
	constructor(radius: number, angle: number) {
		this.coordinates = {radius: radius, angle: angle};
	}
	setPolar(radius: number, angle: number):void {
		this.coordinates.radius = radius;
		this.coordinates.angle = angle;
	}
	setXY(x: number, y: number): void {
		this.coordinates.radius = Math.sqrt(x * x + y * y);
		this.coordinates.angle = Math.atan2(y, x);
	}
}

クラスには、implementsキーワードで複数のインタフェースが実装できます。クラスの側でインタフェースの組み合わせを選ぶかたちになります。


interface IPoint {
	setXY(x: number, y: number): void;
}
interface IPolar {
	setPolar(radius: number, angle: number): void;
}
interface ICoordsXY extends IPoint {
	coordinates: {x: number, y: number};
}
class Point implements ICoordsXY, IPolar {

}

複数のインタフェースを実装したクラスの例が以下のコード002です。つぎのようなコードで結果が確かめられます。


let point: Point = new Point(1, 2);
point.setPolar(2, Math.PI / 3);
console.log(point.coordinates);
// { x: 1.0000000000000002, y: 1.7320508075688772 }

コード002■複数のインタフェースを実装したクラス


interface IPoint {
	setXY(x: number, y: number): void;
}
interface IPolar {
	setPolar(radius: number, angle: number): void;
}
interface ICoordsXY extends IPoint {
	coordinates: {x: number, y: number};
}
class Point implements ICoordsXY, IPolar {
	coordinates: {x: number, y: number};
	constructor(x: number, y: number) {
		this.coordinates = {x: x, y: y};
	}
	setXY(x: number, y: number): void {
		this.coordinates.x = x;
		this.coordinates.y = y;
	}
	setPolar(radius: number, angle: number): void {
		this.coordinates.x = radius * Math.cos(angle);
		this.coordinates.y = radius * Math.sin(angle);
	}
}
let point: Point = new Point(1, 2);
point.setPolar(2, Math.PI / 3);
console.log(point.coordinates);
// { x: 1.0000000000000002, y: 1.7320508075688772 }

09 ハイブリッド型

これまでご紹介した型を組み合わせたインタフェースも定められます。たとえば、関数の型づけに、オブジェクトとしてのプロパティやメソッドを加えることです。


interface ICoordinates {
	x: number;
	y: number;
}
interface IPointFunction {
	(x: number, y: number): ICoordinates;
	coordinates: ICoordinates;
	setPolar(radius: number, angle: number): void;
}
let point: IPointFunction = <IPointFunction>function (x: number, y: number): ICoordinates {
	let coords: ICoordinates = {x: x, y: y};
	(arguments.callee as IPointFunction).coordinates = coords;
	return coords;
}
point.setPolar = function (radius: number, angle: number): void {
	this.coordinates.x = radius * Math.cos(angle);
	this.coordinates.y = radius * Math.sin(angle);
}
point(0, 0);
console.log(point);
// { [Function: point] setPolar: [Function], coordinates: { x: 0, y: 0 } }
point.setPolar(2, Math.PI / 3);
console.log(point.coordinates);
// { x: 1.0000000000000002, y: 1.7320508075688772 }

10 クラスでインタフェースを拡張する

インタフェースは、クラスで拡張することもできます。このとき、クラスのプロパティやメソッド(メンバー)は、たとえアクセス制限がprivateであっても、実装は除いてすべて継承されます。インタフェースにprivateprotectedのメンバーが含められるのです。そうすると、サブクラスしかこのインタフェースは実装できません。


class Point {
	private x: number;
	private y: number;
}
interface IPolar extends Point {
	setXY(x: number, y: number): void;
}
class Vector extends Point {
	constructor() {
		super();
	};
	setXY(x: number, y: number): void {}
}
let vector: IPolar = new Vector();

privateプロパティの備わるクラスをインターフェイスで拡張して、サブクラスに実装する例が以下のコード003です。つぎのようなスクリプトで、結果が確かめられます。


let vector: IPolar = new Vector(1, Math.sqrt(3));
let polar = vector.getPolar();
console.log(polar.radius, polar.angle * 180 / Math.PI);
// 1.9999999999999998 59.99999999999999

コード003■インタフェースをクラスで拡張する


interface ICoordinates {
	x: number;
	y: number;
}
interface IPoint {
	getXY(): ICoordinates;
}
class Point implements IPoint {
	constructor(private x: number, private y: number) {}
	getXY(): ICoordinates {
		return {x: this.x, y: this.y};
	}
}
interface IPolar extends Point {
	getPolar(): {radius: number, angle: number};
}
class Vector extends Point implements IPolar {
	constructor(x: number, y: number) {
		super(x, y);
	}
	getPolar() {
		let coords: ICoordinates = this.getXY();
		let x: number = coords.x;
		let y: number = coords.y;
		let radius: number = Math.sqrt(x * x + y * y);
		let angle: number = Math.atan2(y, x);
		return {radius: radius, angle: angle};
	}
}


作成者: 野中文雄
作成日: 2017年2月13日


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