TypeScriptでは、イミュータブルなクラスを作りたいときに、クラスプロパティにreadonly
をつけることで、読み取り専用のプロパティを定義できる。readonly
なプロパティに値を再代入しようとすると、コンパイルエラーになる。
TypeScriptコードはコンパイルされるとJavaScriptコードになるが、そのときにはreadonly
の情報は削除される。したがって、もともとreadonly
だったクラスプロパティは、JavaScriptコードになったときには上書き可能なプロパティになってしまう。
TypeScriptだけでソースコードを固められる場合は、readonly
つまりコンパイル時のイミュータビリティチェックだけで十分だと思う。一方で、TypeScriptで書いたクラスをJavaScriptライブラリとして提供し、かつ、そのクラスが誰に使われるか分からないような場合には、実行時もイミュータブルなクラスになっていないと不安が残る。
本稿では、デコレーターを使った、実行時イミュータブルなクラスの実現方法を紹介する。
実行時イミュータブルにするデコレータの定義
function immutable(constructor: any): any {
const wrapper = function () {
const instance = new constructor(...arguments)
Object.freeze(instance)
return instance
}
wrapper.prototype = Object.create(constructor.prototype)
return wrapper
}
このデコレーター関数は、クラスコンストラクタを引数に受け取り、それを継承したプロキシコンストラクタを返すようになっている。
プロキシコンストラクタの中身は特に変わったことはしてなく、親コンストラクタが作ったインスタンスにObject.freeze
をかけているだけだ。
デコレータの使い方
@immutable
class Sample {
constructor(public readonly foo: number) {
}
}
クラス定義に@immutable
と書くだけ。
本当に実行時イミュータブルになっているかの確認
import assert = require('assert')
const sample = new Sample(0)
assert.strictEqual(sample.foo, 0)
assert.throws(
() => eval(`sample.foo = 1`),
/^TypeError: Cannot assign to read only property 'foo' of object '#<Sample>'$/
)
assert.strictEqual(sample.foo, 0)