TypeScript
Swift
Firebase
cloudfunctions

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

Cloud Functions for Firebase を TypeScript で書いていると、 Firebase Realtime DatabaseCloud Firestore を使っている時にモデルをいい感じに定義することができます。

Cloud Function + TypeScript の環境構築は Cloud Functions を TypeScript で書く - Qiita を参照してください。

具体的にどういうこと

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

iOS

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

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

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

スクリーンショット 2017-11-19 3.48.53.png

* ここでは Salada というライブラリを使っています。
* 詳しくは Firebase Model Framework Saladaを使ってUser Modelを作る - Qiita などを参照してください。

Cloud Functions

any 型

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

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

model 定義をする

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

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

  interface Salada {
    _createdAt: number
    _updatedAt: number
  }
}

export default firebase

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

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:

もっと便利に使う

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:

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:

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

  interface Salada {
    _createdAt: number
    _updatedAt: number
  }
}

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

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: