Edited at

TypeScriptのDecoratorを使ってserializableを実現する

More than 3 years have passed since last update.

JavaのSerializableのようなものをJavaScriptで!

デコレータ使ってJSONのstringify/parseでオブジェクトを簡単に復元できるようにしてみる。


toJSON

前知識。

JavaScriptの仕様として、オブジェクトにtoJSONを実装しておくとJSON.stringifyの時に参照してくれる。

別のオブジェクト内部にあってもOK。

var obj = {

a: 1,
b: {
c: 1,
toJSON: function(){ return 'hoge'; }
}
};

JSON.stringify(obj); // => "{"a":1,"b":"hoge"}"

これオライリーのJavaScript本読むまで知らなかった。みんなちゃんと定義書読もうね。


parse

また、JSON.parseにも第二引数に関数を設定して処理を挟める。

JSON.parse('{"a":1,"b":2}', function(key,value) {

if(key === 'b') {
return 'piyo';
} else {
return value;
}
}); // => Object {a: 1, b: "piyo"}

前知識ここまで。


Decoratorでオブジェクト復元

TypeScriptのDecorator使ったらオブジェクト簡単に復元できんじゃね?ってことで、

Decorator使って、クラスにtoJSONを生やすものを書いてみた。

追記: コンストラクタの格納方法を修正。


serializable.ts

const constructors = {}; // コンストラクタ格納

// class用decorator (toJSONを生やす)
export const serializable = (target: any) => {
constructors[target.name] = target;
target.prototype.toJSON = function(key) {
if (key === '__serializeValue') { // 入れ子ループ防止用
return this;
} else {
// デシリアライズに必要な情報を埋める
return {
__serializeName: target.name,
__serializeValue: this
};
}
};
};

// JSON.parse用関数
export const deserialize = (k:string, v:any) => {
// 関係ないオブジェクトは通常処理
if(!v.__serializeName) return v;

if (constructors[v.__serializeName].fromJSON){
// fromJSONが定義されていればそれを利用
return constructors[v.__serializeName].fromJSON(v.__serializeValue);
} else {
// 定義されていなければ無引数でコンストラクタ実行して値設定
const obj = new constructors[v.__serializeName]();
for (let key in v.__serializeValue) {
if (v.__serializeValue.hasOwnProperty(key)) {
obj[key] = v.__serializeValue[key];
}
}
return obj;
}
};


こうやって使う。

import {serializable, deserialize} from './serializable'

@serializable // シリアライズ可能にする!
class Hoge{
piyo: number;
fuga: number;
piyofuga() {
return this.piyo + this.fuga;
}
}

const hoge = new Hoge();
hoge.piyo = 3;
hoge.fuga = 2;
console.log(hoge.piyofuga()); // => 5

// 文字列化
const json = JSON.stringify(hoge);
console.log(json);
// => {"__serializeName":"Hoge","__serializeValue":{"piyo":3,"fuga":2}}

// 復元
const obj = JSON.parse(json, deserialize);
console.log(obj.piyofuga()); // => 5

ただしこの場合、無引数でコンストラクタが実行されてしまう。

なので、静的関数fromJSONがあればそれを使うようにしてある。

import {serializable, deserialize} from './serializable'

@serializable
class Hoge{
constructor(piyo: number, fuga:number) {
this.piyo = piyo;
this.fuga = fuga;
console.log(`Created! piyo:${piyo} fuga:${fuga}`);
}
// JSONからの復元方法を指定
static fromJSON(json: {piyo: number, fuga:number}):Hoge {
return new Hoge(json.piyo, json.fuga);
}
}

const hoge = new Hoge(3, 2); // => "Created! piyo:3 fuga:2"
JSON.parse(JSON.stringify(hoge), deserialize); // => "Created! piyo:3 fuga:2"

// fromJSONがない場合
delete Hoge.fromJSON;
JSON.parse(JSON.stringify(hoge), deserialize); // => "Created! piyo:undefined fuga:undefined"

ちなみに、別段Decorator使わなくても普通に呼んでもいい。

むしろこっちのほうが汎用的かもしれない。(ええ...)

class Hoge{

piyo: number;
fuga: number;
piyofuga() {
return this.piyo + this.fuga;
}
}

serializable(Hoge); // toJSON生やす

状態保存に使えたりしないかな。