サイトトップ

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

HTML5テクニカルノート

TypeScript 2.0のreadonly修飾子


TypeScript 2.0にreadonly修飾子が備わりました。この修飾子で読み取り専用のプロパティ、つまり定数が定められます。

01 これまでの定数の定め方

これまでのTypeScriptでも、読み取り専用の変数やプロパティを定めることはできました。ここでは、つぎのクラス(Point)を例にして、定数の扱いをご説明しましょう。なお、このクラスはTypeScript入門01から同06までの解説で書いたコードから、一部を抜き出して簡単にしたものです。具体的な中身については、それらのノートをお読みください。コンストラクタには引数として(x, y)座標を与えます。ふたつのgetアクセサで、それぞれ座標の原点からの距離(length)およびx軸となす角度(angle)が得られます。


class Point {
	constructor(public x: number = 0, public y: number = 0) {
	}
	get length(): number {
		var square: number = this.x * this.x + this.y * this.y;
		return Math.sqrt(square);
	}
	get angle(): number {
		return Math.atan2(this.y, this.x);
	}
}

getアクセサで得られる角度(angle)は、単位がラジアンです。わかりやすくするために、ラジアンと度数との変換比率(180/π)を定数に定めます。変数にconst宣言すると、値は書き替えられなくなります。変数に他の値を入れようとすれば、つぎのエラーが示されます。

Left-hand side of assignment expression cannot be a constant or a read-only property.

前掲クラス(Point)でつぎのコードを試せば、座標(1, √3)の原点からの距離(2)およびx軸となす角度(60度)が得られます。


const RAD_TO_DEG: number = 180 / Math.PI;
let obj: Point = new Point(1, Math.sqrt(3));
// RAD_TO_DEG = 0;  // エラー
console.log(obj.length, obj.angle * RAD_TO_DEG);  // 1.9999999999999998 59.99999999999999

もっとも、const宣言はクラスのプロパティには使えません。クラスのプロパティに定めようとすると、つぎのエラーが示されます。

A class member cannot have the 'const' keyword.

そこで用いるのが、getアクセサです。setアクセサがなければ、読み取り専用プロパティとして扱えます。つぎのコードはクラス(Point)に静的なgetアクセサ(RAD_TO_DEG)を定めて、定数にしました。


class Point {
	constructor(public x: number = 0, public y: number = 0) {
	}
	get length(): number {
		var square: number = this.x * this.x + this.y * this.y;
		return Math.sqrt(square);
	}
	get angle(): number {
		return Math.atan2(this.y, this.x);
	}
	static get RAD_TO_DEG(): number {
		return 180 / Math.PI;
	}
}

以下のようにクラス定数(Point.RAD_TO_DEG)として使え、書き替えはできません。値を代入しようとすれば、setアクセサがありませんので、つぎのエラーが示されます。

Left-hand side of assignment expression cannot be a constant or a read-only property.

let obj: Point = new Point(1, Math.sqrt(3));
// Point.RAD_TO_DEG = 0;  // エラー
console.log(obj.length, obj.angle * Point.RAD_TO_DEG);  // 1.9999999999999998 59.99999999999999

02 TypeScript 2.0のreadonly修飾子で定数を定める

TypeScript 2.0に備わったreadonly修飾子を用いれば、クラスに読み取り専用のプロパティが定められます。前掲のクラス(Point)でgetアクセサに替えて、readonly修飾子で定数(RAD_TO_DEG)を加えたのがつぎのコードです。値の書き替えはできなくなりますので、前掲クラスと同じテスト結果が得られます(書き替えようとすれば、エラーが示されます)。ただし、後述するとおり、readonly修飾子とgetアクセサを使った場合とでは、コンパイルされたJavaScriptファイルのコードは異なります。


class Point {
	static readonly RAD_TO_DEG: number = 180 / Math.PI;
	constructor(public x: number = 0, public y: number = 0) {
	}
	get length(): number {
		var square: number = this.x * this.x + this.y * this.y;
		return Math.sqrt(square);
	}
	get angle(): number {
		return Math.atan2(this.y, this.x);
	}
}

readonly修飾子は、インタフェース(interface)に用いることもできます。実装(implements)したクラスは、そのプロパティを与えられた型で備えなければなりません。なお、クラスの側ではreadonly修飾子を省いても、そのプロパティは読み取り専用とみなされます。


interface IId {
	readonly id: number;
}
class Id implements IId {
	readonly id: number = 0;
}

さらに、配列を読み取り専用にする型づけReadonlyArrayが、新たに備わりました。後に添える<>の中に、エレメントの型を与えます。すると、つぎのように配列(array)の内容を変えることはできなくなります。その参照を読み取り専用でない配列型の変数(temp)に代入することもできません。


let array: ReadonlyArray<number> = [0, 1, 2];
array[2] = 5;  // エラー
// Property 'push' does not exist on type 'ReadonlyArray<number>'.
array.push(5);  // エラー
// Property 'push' does not exist on type 'ReadonlyArray<number>'.
array.length = 2;  // エラー
// Left-hand side of assignment expression cannot be a constant or a read-only property.
let temp: number[] = array;  // エラー
// Type 'ReadonlyArray<number>' is not assignable to type 'number[]'.

読み取り専用でない配列は、ReadonlyArrayで型づけした変数に納められます。その場合、もちろん変数の配列は読み取り専用です。ただし、もとの配列が、つぎのように別の読み取り専用でない変数(temp)に入っていたときは、その変数の配列は書き替えできてしまいます。ふたつの変数は同じ配列を参照しますので、結果として読み取り専用の配列(array)の内容が変わることにご注意ください。


let temp: number[] = [0, 1, 2];
let array: ReadonlyArray<number> = temp;
temp[2] = 5;
console.log(array);  // [0,1,5]

また、読み取り専用の配列を型変換することでも、読み取り専用でない変数(temp)に入れてしまえます。すると、代入先の変数で、値の書き替えができることになります。


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

03 readonly修飾子とgetアクセサとの違い

readonly修飾子で定めたプロパティの、getアクセサとの違いのひとつは、つぎのようにクラス(Id)のコンストラクタだけは値を書き替えられることです。


interface IId {
	readonly id: number;
}
class Id implements IId {
	readonly id: number = 0;
	constructor(id?: number) {
		if (id) {
			this.id = id;
		}
	}
}

したがって、同じクラスのインスタンスそれぞれに、コンストラクタで読み取り専用の異なるプロパティ値が定められることになります。もちろん、その後の書き替えはできません(エラーになります)。


let objects: IId[] = [];
for (let i: number = 0; i < 3; i++) {
	objects.push(new Id(i));
}
// objects[0].id = 5;  // エラー
console.log(objects);
// [ Id { id: 0 }, Id { id: 1 }, Id { id: 2 } ]

もうひとつ、内部的な扱いの違いについてご説明しましょう。readonly修飾子で定めたプロパティの処理は、ビルドのときに確かめられ、書き替えがあればエラーを示します。けれど、たとえば前掲のクラス(Point)のreadonly修飾子による定数(RAD_TO_DEG)は、コンパイルされたJavaScriptファイルではつぎのようにクラスのコンストラクタに定められます。つまり、書き出されたコードの値そのものは、変えてしまえるということです(テストコード)。


Point.RAD_TO_DEG = 180 / Math.PI;

それに対して、getアクセサで定数(RAD_TO_DEG)を定めたとき、ビルドされたJavaScriptファイルではObject.defineProperty()メソッドでプロパティを加えます。そして、つぎのようにアクセサディスクリプタはgetのみでsetがありませんので、JavaScriptコードとしても書き替えられません(テストコード)。コンパイルしたJSファイルを読み込んで、TypeScriptでなくJavaScriptコードで使うときはgetアクセサを用いるほうが安心でしょう。


Object.defineProperty(Point, "RAD_TO_DEG", {
	get: function () {
		return 180 / Math.PI;
	},
	enumerable: true,
	configurable: true
});


作成者: 野中文雄
更新日: 02「TypeScript 2.0のreadonly修飾子で定数を定める」に型変換した代入について追記。
作成日: 2016年10月21日


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