LoginSignup
29
18

More than 5 years have passed since last update.

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

Last updated at Posted at 2016-02-08

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生やす

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

29
18
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
29
18