Cloud Functions を TypeScript で書いて Realtime Database のモデル定義をいい感じにする

[Cloud Functions for Firebase](https://firebase.google.com/docs/functions/?hl=ja) を TypeScript で書いていると、 [Firebase Realtime Database](https://firebase.google.com/docs/database/?hl=ja) や [Cloud Firestore](https://firebase.google.com/docs/firestore/?hl=ja) を使っている時にモデルをいい感じに定義することができます。

Cloud Function + TypeScript の環境構築は [Cloud Functions を TypeScript で書く \- Qiita](https://qiita.com/star__hoshi/items/7dcf5970d28a7ff239fb) を参照してください。

# 具体的にどういうこと

Realtime Database を使っていて、クライアント側は iOS のケースで書いていきます。

## iOS

Firebase を使う際に、クライアント側でモデル定義をすると思います。
例えば、 User Model を以下のように定義したとします。

```swift
class User: Object {
    @objc dynamic var name: String?
    @objc dynamic var age: Int = 0
}
```

Realtime Database にはこのように保存されます。

<img width="507" alt="スクリーンショット 2017-11-19 3.48.53.png" src="https://qiita-image-store.s3.amazonaws.com/0/5678/33c41567-d547-bc78-b50b-5f79c4752a3f.png">


\* ここでは [Salada](https://github.com/1amageek/Salada) というライブラリを使っています。
\* 詳しくは [Firebase Model Framework Saladaを使ってUser Modelを作る \- Qiita](https://qiita.com/1amageek/items/4fbba1b22e81b650d656) などを参照してください。


## Cloud Functions

### any 型

Cloud Functions で User データを取得する時はこのようにしてデータを取得しますが、これだと user が any 型になってしまいます。

```ts
const user = await admin.database()
                        .ref('v1/user/-Kxd4TYd_MgjEYhmEO0p')
                        .once('value')
                        .then(snap => snap.val())
```

### model 定義をする

このようなモデル定義を書きます。

```ts
declare namespace firebase {
  interface User extends Salada {
    name?: string
    age: number
  }

  interface Salada {
    _createdAt: number
    _updatedAt: number
  }
}

export default firebase
```

そして user データ取得時に firebase.User にキャストしてあげます。

```ts
const user = <firebase.User> await admin.database()
                                        .ref('v1/user/-Kxd4TYd_MgjEYhmEO0p')
                                        .once('value')
                                        .then(snap => snap.val())
```

こうすると user 型として扱えるため、 name が optional であることも示せます。
また、**補完が効く**ようになり開発効率も上がります。

any 型に対し `user.name` と書くことも可能ですが、 `user.nema` としてもエラーにはならないのでやはり型があると安心できますね :relaxed: 

## もっと便利に使う

```ts
const user = <firebase.User> await admin.database()
                                        .ref('v1/user/-Kxd4TYd_MgjEYhmEO0p')
                                        .once('value')
                                        .then(snap => snap.val())
```

この書き方でも便利でいいのですが、 user オブジェクトに id がないのが不満でした。
`-Kxd4TYd_MgjEYhmEO0p` が userID なのですが、 admin.database().ref で取得すると id 自体の情報は失われてしまいます。

### user._id でアクセスできるようにする

Ref という Util クラスを作ります。　
やることは単純で、 `Referenceable` を extends している Model であれば取得したデータに _id プロパティをつけているだけです。

ref.ts:

```ts
export class Ref {
  static async snapshot<T extends Referenceable>(path: RefPath, id: string): Promise<T> {
    const value: T = await admin.database().ref(`${path}/${id}`).once('value').then(snap => snap.val())
    value._id = id

    return value
  }
}

export enum RefPath {
  User = '/v1/user'
}

export interface Referenceable {
  _id: string
}
```

firebase.ts: 

```ts
declare namespace firebase {
  interface User extends Salada, Referenceable {
    name?: string
    age: number
  }

  interface Salada {
    _createdAt: number
    _updatedAt: number
  }
}
```

この Util クラスを使ってデータを取得すると _id でアクセスできるようになります。

```ts
const user = <firebase.User> await Ref.snapshot(RefPath.User, '-Kxd4TYd_MgjEYhmEO0p')
user._id // => -Kxd4TYd_MgjEYhmEO0p
```

Realtime Database の path も1箇所で定義できて良いですね。

本当は Path は `T` の Type をみて自動で設定したかったのですが、 TypeScript の Interface は js にすると存在がなくなってしまうようでできませんでした :cry: