class-transformerでpureなjsオブジェクトとクラスとの変換が便利だった


この記事は[Wanoグループ Advent Calendar](https://qiita.com/advent-calendar/2017/wano-group) 2017の2日目の記事になります。
なんか忘れてた気がするけど断じて2日目なのです。

## クライアントで扱うエンティティの「型」
jsでAPIからエンティティをやり取りするとき、json.stringifyやjson.parseを使っていると、クライアント用に変換されるデータは単純なobjectとなります。
特にアーキテクチャとしてRedux等を使っている方は、クライアント側が保持するデータをclass等でなく純粋なjsonと等価になるobjectで保持していることが多いかと思います。
その形式で保持することは、特にサーバーサイドレンダリングのやりやすさなどを考えると妥当な手段ですが、エンティティをクラスとして扱ってメソッドを生やしたい、という時には取り回しが悪いです。
特に僕はアプリのステートを[mobx](https://github.com/mobxjs/mobx)　を使っており、基本的にはステートをclassで扱っているので、 SSRしたステートのクライアントサイドでの復元にも困っていました。 

ここではclassとobjectを雑に相互変換する、[typestack/class-transformer](https://github.com/typestack/class-transformer)を紹介したいと思います。


## 基本
基本的な使い方は簡単で、まず、エントリーポイントとなるjsなどに

```typescript
import "reflect-metadata";

```
を読み込みます。

あとは使用するだけです。

```json
{
  "id" : 200,
  "name" : "hoge",
  "point" : 0
}
```
というjsonオブジェクトがあったとして、以下のようなclassに変換したいときは、

```typescript
class SampleEntity {
    id? : number;
    name : string;
    point : number;
    
    public increment(){
        this.point++;
    }
}
```

次のようににリフレクションかけるだけです。

```typescript
import {plainToClass} from "class-transformer";

const sampleJson = {
  "id" : 200,
  "name" : "hoge",
  "point" : 0
};

class SampleEntity {
    id? : number;
    name : string;
    point : number;
    
    public increment(){
        this.point++;
    }
}

const sampleEntity = plainToClass(SampleEntity, sampleJson);
sampleEntity.increment(); // point = 1

```
問題なくSampleEntityのメソッドを使用することができます。

## ネストしたクラス構造

```typescript

class Parent {
    id? :number;
    name : string;

    @Type(() => Child)
    nestedChild : Child

    incrementChildPoint() : void{
        this.nestedChild.increment()
    }
}

class Child {
    name : string;
    point : number;
    increment(){
        this.point++
    }
}

const sampleJson2 = {
    "id" : 1,
    "name" : "parent",
    "nestedChild" : {
        "name" : "child",
        "point" : 300
    }
}

const parentEntity = plainToClass(Parent , sampleJson2);
parentEntity.incrementChildPoint() // 301

```
Typesデコレータによって、変換時にnestedChildプロパティはChildクラスとして変換されます。
このように、同梱されているデコレータによって、様々なclassへの変換や別名を与えることができます。
この辺はGoのタグでやっていることとほぼ同じような感じですね。

### 文字列とDateを相互変換する


```typescript
const fetchedArticleJson = {
    "id" : 2,
    "text" : "hogehoge",
    "release_time" : "2017-10-15T10:02:15+09:00",
    "create_time" : "2017-10-05T10:02:15+09:00"
}

class Article {
    id? :number;
    text: string;
    release_time : Date;
    
    @Type(() => Date)
    create_time : Date;
}

const fetchedArticle : Article = plainToClass(Article , fetchedArticleJson);
console.log(fetchedArticle.create_time.getTime()) //1507165335000
console.log(fetchedArticle.release_time.getTime()) //fetchedArticle.release_time.getTime is not a function',

```

サーバから受け取るjsonでは、時間を扱うプロパティも単なる文字列ですが、変換時にDateになるように指定できます。
デコレータをつけていない ``` create_time ``` の方は、単なる文字列のままとしての扱いなため、``` getTime() ``` が生えません。

アプリ用エンティティをサーバにjsonとして送るときは、

```typescript
const serializedArticle = classToPlain(fetchedArticle);

```

として戻してやればOKです。

### Transformで自由にフックする

プロパティを、jsで時間を扱う時に有用な [moment](https://momentjs.com/) のオブジェクトとして扱いたい、という時も、class変換時、プレーンなjs変換時で自由にフック処理が書けます。

```typescript
export class Photo {

    id: number;

    @Type(() => Date)
    @Transform(value => moment(value), { toClassOnly: true })
    date: Moment;

    getReadableDate() : string {
        return this.date.format("YYYY[年]MM[月]DD[日]  HH[時]mm[分]ss[秒]")
    }
}

const photoJson = {
    "id" : 300,
    "date" : "Thu, 06 Sep 2012 10:00:00 +0900"
}


const photo : Photo = plainToClass(Photo , photoJson);
photo.getReadableDate()) //2012年09月06日  10時00分00秒

```

この時、Transformフックはそのままでは サーバ送信用に ```classToPlain(photo) ``` を書けた時も実行されてしまうため、```{ toClassOnly: true } ``` オプションを有効にしておきます。


### string <=> enum変換

ちょっと例が悪いのですが、数値をgetしてきたときはstringとして扱い、サーバーにput処理かけるときはまた数値に戻す...といった手順は例えば以下のようになります。

```typescript

type colorType = "red" | "blue" | "green"
enum COLOR {
    RED = 1,
    BLUE,
    GREEN
}

const colorToString = (i : COLOR)=>{

    const mapping = {
        [COLOR.RED] : "red",
        [COLOR.BLUE] : "blue",
        [COLOR.GREEN] : "green",
    }

    return mapping[i]
}

const colorTypeToENUM = (str : string)=>{

    const mapping = {
        "red" :  COLOR.RED,
        "blue" :   COLOR.BLUE ,
        "green" :  COLOR.GREEN ,
    }

    return mapping[str]
}

class ColorInfo {
    // put時、get時でそれぞれ変換
    @Transform(value => colorToString(value) , { toClassOnly: true })
    @Transform(value => colorTypeToENUM(value) , { toPlainOnly: true })
    base_color : colorType

}

```
同じプロパティへのデコレータ増えて見通し悪いときは、1つの高階関数にまとめてもいいかもしれません。

## まとめ
他にもいろんなオペレータあるのですが、とりあえずDate変換だけでも便利ですので、是非使ってみてほしいと思います。