TypeScript/ES6+ES7でクラスメンバーの追加を阻止

ガチガチに固めた、静的な言語のようなクラスをJavaScript(TypeScript+ES6+ES7)で作ってみるシリーズ。


## クラスメンバーの追加を阻止する

JavaやC#などのクラスは基本的には定義後にメンバープロパティを追加できない。TypeScriptも同様だが、あくまでもコンパイル時チェックであって、実行時は普通のjsなので追加できてしまう。

ES5の機能に`Object.preventExtensions()`,`Object.seal()`,`Object.freeze()`があり、これらを使うとクラスメンバープロパティの追加を阻止できる[^1]。

```javascript:プロパティを後から追加できないクラス
"use strict";
class A{
	constructor(){
		Object.preventExtensions(this);
	}
	prop = 1;
}

//実行
var a = new A();
a.prop2 = 100;		//error!:Uncaught TypeError: Can't add property prop2, object is not extensible
```

[TypeScript PlayGround](http://www.typescriptlang.org/Playground#src=%22use%20strict%22%3B%0Aclass%20A%7B%0A%09constructor()%7B%0A%09%09Object.preventExtensions(this)%3B%0A%09%7D%0A%09prop%20%3D%201%3B%0A%7D%0A%0A%2F%2F%E5%AE%9F%E8%A1%8C%0Avar%20a%20%3D%20new%20A()%3B%0Aa.prop2%20%3D%20100%3B%09%09%2F%2Ferror!%3AUncaught%20TypeError%3A%20Can't%20add%20property%20prop2%2C%20object%20is%20not%20extensible) | [Babel](https://babeljs.io/repl/#?experimental=true&evaluate=true&loose=false&spec=false&code=class%20A%7B%0A%20%20%20%20constructor()%7B%0A%20%20%20%20%20%20%20%20Object.preventExtensions(this)%3B%0A%20%20%20%20%7D%0A%20%20%20%20prop%20%3D%201%3B%0A%7D%0A%0A%2F%2F%E5%AE%9F%E8%A1%8C%0Avar%20a%20%3D%20new%20A()%3B%0Aa.prop2%20%3D%20100%3B%20)

上記のコードで[^2]はコンストラクタで`Object.preventExtensions()`の対象にクラスインスタンス(`this`)を指定する事ですべてのクラスインスタンスへのプロパティ追加を阻止している。

なお、この方法ではTypeScriptのコンパイル時チェックをすり抜ける、ブラケットアクセスであっても阻止できる。

```javascript:ブラケットアクセスでも阻止
a["prop2"] = 100;	//runtime error!
```

### ３つのメソッドの違い

 1. プロパティの値を変更・削除してもいい
  * `Object.preventExtensions()`
  * ただし、プロパティが関数の場合は再割当禁止
 2. プロパティの値を変更してもいいが削除はダメ
  * `Object.seal()`
 3. プロパティの値の変更もダメ
  * `Object.freeze()`


```javascript:クラス
//base
class A{
	prop = 1;
	constructor(){
		/* Object.*(this); */
	}
	func(){}
}
var a = new A();
```
```javascript:３パターンの違い
//Object.preventExtensions()
a.prop = 1000;	//OK
delete a.prop;	//OK
a.func = () => {};	//NG
a.prop = () => {};	//OK (TSではNG)

//Object.seal()
a.prop = 1000;	//OK
delete a.prop;	//NG
a.func = () => {};	//NG
a.prop = () => {};	//OK (TSではNG)

//Object.freeze()
a.prop = 1000;	//NG
delete a.prop;	//NG
a.func = () => {};	//NG
a.prop = () => {};	//NG
```

JavaやC#などのクラスには`preventExtensions()`や`seal()`を指定した時がイメージが近い。`freeze()`は全てがreadonlyなので、Javaでいえば全部のメンバー変数に`final`/C#で言えば全部のメンバー変数に`readonly`を指定したのと近い。

## デコレータでアクセス修飾子風に指定する

上記で無事実現できたが、１行とは言え毎回コンストラクタ内で指定するのは他のコードに混じって意味がわかりにくい。デコレータをつかうことで、アクセス修飾子風に記述することができる。

```javascript:デコレータでアクセス修飾子
@seal class A{
	constructor(){}
	prop = 1;
}
```

デコレータ自体の定義は以下。コンストラクタを置き換えるためやや長いが一度定義してしまえばあとは簡単。

```javascript:@sealデコレータの定義
function seal<T extends Function>(t:T):T|void{
	let base = t;
	
	function construct(constructor:T, args:any[]) {
		let c: any = function () {
			return constructor.apply(this, args);
		};
		c.prototype = constructor.prototype;
		return new c();
	}
	
	let func: any = (...args:any[])=>{
		let a = construct(base, args);
		Object.seal(a);	//ここで指定
		return a;
	};
	
	func.prototype = base.prototype;
	
	return func;
}
```

```javascript:実行時
var a = new A();
a.prop = 10;
a.prop2 = 10;	//error!
```

上記コードはES7のデコレータを使うので、TypeScript1.5.3以降、`--experimentalDecorators`オプションを有効にすることで使える。型指定を省けば、[Babelでも有効](https://babeljs.io/repl/#?experimental=true&evaluate=true&loose=false&spec=false&code=function%20seal(t)%7B%0A%09let%20base%20%3D%20t%3B%0A%09%0A%09function%20construct(constructor%2C%20args)%20%7B%0A%09%09let%20c%3A%20any%20%3D%20function%20()%20%7B%0A%09%09%09return%20constructor.apply(this%2C%20args)%3B%0A%09%09%7D%3B%0A%09%09c.prototype%20%3D%20constructor.prototype%3B%0A%09%09return%20new%20c()%3B%0A%09%7D%0A%09%0A%09let%20func%3A%20any%20%3D%20(...args)%3D%3E%7B%0A%09%09let%20a%20%3D%20construct(base%2C%20args)%3B%0A%09%09Object.seal(a)%3B%09%2F%2F%E3%81%93%E3%81%93%E3%81%A7%E6%8C%87%E5%AE%9A%0A%09%09return%20a%3B%0A%09%7D%3B%0A%09%0A%09func.prototype%20%3D%20base.prototype%3B%0A%09%0A%09return%20func%3B%0A%7D%0A%0A%40seal%20class%20A%7B%0A%09constructor()%7B%7D%0A%09prop%20%3D%201%3B%0A%7D%0A%0Avar%20a%20%3D%20new%20A()%3B%0Aa.prop%20%3D%2010%3B%0Aa.prop2%20%3D%2010%3B%20%20%20%2F%2Ferror!)。

※なお、`@sealed`としなかったのはC#の`sealed`とかぶるため。C#の`sealed`は継承不可の指定のため意味合いが異なる。

## 注意点

 * 入れ子のプロパティ、つまりプロパティの子プロパティには適応されない
	 * 入れ子まで指定したい場合は、子や孫までずっと走査して指定する必要がある
 * デコレータの場合は、継承したクラスにも同じくデコレータを指定する必要がある
	 * コンストラクタ内で指定の場合は継承するだけでOK
 * ES6の範囲内だけでやる場合、コンストラクタ内で変数プロパティを宣言後に`Object.seal(this)`とする必要がある
	 * 後からやらないと一切追加できなくなるため


[^1]: なのでES3環境では実現できない。
[^2]: このコードはTSとexperimentalフラグをONにしたBebelで有効。