TypeScriptのNext.jsやExpoでFirestoreを型安全に、そして楽に使えるようにヘルパー関数を書いた

gm! ELSOUL LABO の kishi.solです。

今回はTypeScriptのNext.jsやExpoでFirestoreを型安全に、そして楽に使えるようにヘルパー関数を書いた話です。

FirestoreをTypeScriptで型安全に使うためには、データコンバーターを使いますが、毎度同じような呼び出しを必要としてすこし面倒だったりするため、CRUDを簡単に行うことができるようヘルパー関数を用意しようという取り組みです。

参考:

https://firebase.google.com/docs/reference/js/firestore_.firestoredataconverter


まず、任意のデータ型のコンバーターを作成する`createFirestoreDataConverter`関数を定義します。

```typescript
import {
  DocumentData,
  FirestoreDataConverter,
  QueryDocumentSnapshot,
} from 'firebase/firestore'

export const createFirestoreDataConverter = <
  T extends DocumentData
>(): FirestoreDataConverter<T> => {
  return {
    toFirestore(data: T): DocumentData {
      return data
    },
    fromFirestore(snapshot: QueryDocumentSnapshot<T>): T {
      return snapshot.data()
    },
  }
}

```

Firestoreは`doc`または`collection`を使ってデータの位置を特定し、ここで上記のデータコンバーターを使いますが、これもそれぞれ`createDocRef`、`createCollectionRef`とまとめます。

```typescript
import { Firestore, doc, DocumentData } from 'firebase/firestore'
import { createFirestoreDataConverter } from './createFirestoreDataConverter'

export const createDocRef = <T extends DocumentData>(
  db: Firestore,
  collectionPath: string,
  docPath?: string
) => {
  if (!docPath) {
    return doc(db, collectionPath).withConverter(
      createFirestoreDataConverter<T>()
    )
  }
  return doc(db, collectionPath, docPath).withConverter(
    createFirestoreDataConverter<T>()
  )
}

```

```typescript
import { createFirestoreDataConverter } from './createFirestoreDataConverter'
import {
  DocumentData,
  collection,
  Firestore,
  CollectionReference,
} from 'firebase/firestore'

export const createCollectionRef = <T extends DocumentData>(
  db: Firestore,
  collectionPath: string
): CollectionReference<T> => {
  return collection(db, collectionPath).withConverter(
    createFirestoreDataConverter<T>()
  )
}

```


そして、これらを使いFirestoreにデータを追加する`add`関数を定義します。

```typescript
import {
  Firestore,
  DocumentData,
  serverTimestamp,
  addDoc,
  setDoc,
  DocumentReference,
} from 'firebase/firestore'
import { createDocRef } from './createDocRef'
import { createCollectionRef } from './createCollectionRef'

export const add = async <T extends DocumentData>(
  db: Firestore,
  collectionPath: string,
  params: T,
  id?: string
): Promise<DocumentReference<T>> => {
  try {
    if (id) {
      const docRef = createDocRef<T>(db, collectionPath, id)
      await setDoc(docRef, {
        ...params,
        createdAt: serverTimestamp(),
        updatedAt: serverTimestamp(),
      })
      return docRef
    } else {
      const collectionRef = createCollectionRef<T>(db, collectionPath)
      const data = await addDoc(collectionRef, {
        ...params,
        createdAt: serverTimestamp(),
        updatedAt: serverTimestamp(),
      })
      if (!data) {
        throw new Error('no data')
      }
      return data
    }
  } catch (error) {
    throw new Error(`Error adding document: ${error}`)
  }
}

```

id指定の場合と、ランダムなid(推奨。クエリの速度が上がるなど恩恵があります)両方対応しています。
ほぼ必ずcreatedAt、updatedAtは必要なため、Firestoreの`serverTimestamp()`をデフォルトで入れてあります。

これらのヘルパー関数を使用することで、下記のようにロジックコードの部分を削減することができ、すぐにFirestoreを使うことができるようになります。

例:

```typescript
// before
...
const chatRoomsRef = collection(
  db,
  genUserChatRoomPath(user.uid)
).withConverter(createFirestoreDataConverter<UserChatRoom>())
const docRef = await addDoc(chatRoomsRef, {
  ...data,
  createdAt: serverTimestamp(),
  updatedAt: serverTimestamp(),
})
...


// after
...
const docRef = await add<UserChatRoom>(
db,
genUserChatRoomPath(user.uid),
{
  ...data
)
...
```

このように、Firestoreを扱うために必要なコードを大幅に削減し、呼び出す関数も減るので、脳内メモリの使用も抑えつつ爆速で開発をしていくことができます。

下記リンクにFirestoreのCRUDヘルパー関数全体がありますので、興味のある方はぜひご覧ください。

https://github.com/elsoul/skeet-next/tree/main/src/lib/skeet/firestore

こちらのコードはクライアント用ですが、バックエンドのFirebase Adminを使用する場合のヘルパー関数はnpmモジュールとして提供しています。 (実はクライアント用もこちらのnpmパッケージに乗せて公開しようとしていたのですが、Firestoreのdbインスタンスがインストールしたnpmモジュール上では使うことができず、フロントエンドコードにヘルパー関数として配置しておく必要がありました。このあたりもし解決できる方法があればご教示いただければとても嬉しいです。)

https://github.com/elsoul/skeet-firestore

Skeetを使えば、バックエンド及びフロントエンドで使用方法が同じヘルパー関数を使って、Firestoreを型安全にかつ簡単に取り扱うことができます。ドキュメントの型も`skeet sync models`で共有ができるので、バックエンドとフロントエンドの両方から安全にデータを取り扱うことができます。

これは開発スピードを上げるだけでなく、Firestoreを用いた開発で起こりがちな、タイプ違いのデータがどこからか保存されてしまい、検索やその他の機能が壊れてしまうといったような問題を防ぐこともできます。

## Google Cloud、Firebase 上でサーバーレスアプリを爆速開発できるオープンソースの Skeet フレームワーク


Skeet は GCP (Google Cloud) と Firebase 上にフルスタックアプリを構築できるオープンソースの TypeScript 製サーバーレスフレームワークです。

Skeet を使えば、API サーバーから Web・iOS・Android アプリまですべてを TypeScript で爆速開発することができます。GraphQL や Firestore など、開発者体験の評判が良い技術を積極的に採用しています。ChatGPT や Vertex AI などの AI を活用したアプリケーション開発や Solana などのブロックチェーンを活用した Web3 dApp 開発も簡単に行うことができる現代的なアプリケーションフレームワークとなっています。

下記リンクからデモをお試しいただけます。PaLM2・Vertex AI、そして Open AI 社の ChatGPT (GPT-3.5, GPT-4)も同時にお試しいただくことができますので、どちらがどのような特徴を持っているか比較検討していただくことができます。

Skeet デモ: 

https://skeeter.dev/ja/

また、こちらのデモのアプリは Skeet CLI を使えば 5 分でご自身の PC 環境やクラウド環境で動かすことが可能です。本記事のFirestoreヘルパー関数もデフォルトで有効になっているコードが生成されます。

まずはどのようなことができるかデモでイメージしていただき、その後は Skeet CLI を使ってすぐにアプリ開発をスタートできます。

Skeet CLI (GitHub): 

https://github.com/elsoul/skeet-cli

Skeet は世界中すべてのアプリケーション開発現場の開発・メンテナンスコストを削減、開発者体験を向上させるためにオープンソースとして開発されています。

詳しくは公式ドキュメントを御覧ください。

Skeet ドキュメント: 

https://skeet.dev/ja/