JavaScript/TypeScriptのFieldsの取り扱いってどうなんだっけ

2020年02月25日 00時02分

最近 TypeScript で書いているから、従来の動きを追う時に疑問に思ったのでまとめ。

まず、機能名としてはPublic and private instance fields になる。こちらは現在(2020/02/25 時点で)改良中であり、Draft は Stage3 となっている。Stage は 4 段階で Finished になるので、もうしばらく待つ必要がある。詳細はこちら

TypeScript は普通に Fields 記法で記載できるが、コンパイルしたら基本的に constructor に this.[変数名]に変換されている。なので JavaScript で実装を行う場合には constructor 内に定義する必要がある。

インスタンスにプロパティが存在しない項目は、スーパークラスを参照している。Field を参照渡しするように返して、それらを変更するとスーパークラスの Field は変更されてしまうのだろうか。

そうなるとクラスのカプセル化がうまくできていないことになってまずい…

結論

オブジェクト渡すときは気を付けよう…

い つ も の。

TypeScript は ReadOnly 属性を使って避けるようにします。

参照渡しは怖い

JavaScript の場合、オブジェクトは参照渡しになります。

参照渡しは、値ではなくポインタを渡すので、受け取った側が変数の値変更すると受け渡し元の値も変更されます…

参照渡しの場合は副作用が発生する可能性があるのでできれば Field 内はプリミティブ型で設定するのが良いという再確認です。

下記は動作確認したコードです。

field 内は name と weight はプリミティブな型(string, number)で、fooObject はオブジェクトです。

profile()で受け取った値について内容を変更しても、再度 profile()で呼び出した際には値は変更されていませんね。

一方で、getObject()で受け取った値を変更した場合、再度 getObject で呼び出すと値が変更されています。

class Dog {
	private name:string
	private weight:number
	private fooObject
	constructor(name:string, weight:number, fooObject){
		this.name = name
		this.weight = weight
		this.fooObject = fooObject;
	}
	howl(){
		console.log(`${this.name} howling at some people`)
	}
	profile(){
		return {
			name: this.name,
			weight: this.weight
		}
	}
	getObject(){
		return this.fooObject
	}
}

let pochi = new Dog("POCHI", 45, {foo:'bar!'})

let profile = pochi.profile()
console.log(profile);//{ name: 'POCHI', weight: 45 }
profile.name = 'TAMA'
console.log(pochi.profile())//{ name: 'POCHI', weight: 45 };

// objectの場合値が変わる
let fooObject = pochi.getObject();
console.log(fooObject)//{ foo: 'bar!' };
fooObject.foo = 'hoge!'
console.log(pochi.getObject())//{ foo: 'hoge!' };

TypeScript の場合は readonly 定義することでコンパイルエラーになる

TypeScript の場合は readonly を宣言することで変更があった際にはコンパイルエラーで教えてくれるようになっています。

<T>で宣言してしまうと対応できないですが、オブジェクトであっても型を丁寧に定義してあげると副作用が発生しそうな部分で指摘が入ります。

やっぱ TypeScript は最高やな…!

class Dog {
	private name:string
	private weight:number
	private fooObject:{readonly [key:string]: string}//型定義を追加
	constructor(name:string, weight:number, fooObject:{readonly [key:string]: string}){
		this.name = name
		this.weight = weight
		this.fooObject = fooObject;
	}
	getObject():{readonly [key:string]: string}{
		return this.fooObject
	}
}

中略
let fooObject = pochi.getObject();
console.log(fooObject)//{ foo: 'bar!' };
fooObject.foo = 'hoge!'//Index signature in type '{ readonly [key: string]: string; }' only permits reading.
console.log(pochi.getObject())//{ foo: 'hoge!' };

まとめ

無難な結論にはなりますが、オブジェクトで値を管理する時は副作用に気を付けましょう…特にビジネスロジックに触れる場合はプリミティブな型で管理しておく方が良いと思います。

オブジェクトのカプセル化は難しいというのが発見でしたね…

参考

TypeScript Deep Dive日本語版 ReadOnly