[Firestore] Firebase v9でtypescript コード補完をしたい、そしてnext.jsでのエラーを避けたい

```json:package.json
{
  "firebase": "^9.0.2",
  "firebase-admin": "^9.11.1",
}
```

# [client][1]

- v9ではバンドルサイズを最小限にするために、関数ベースに切り替わっている。


```js
// db.collection("...").where("...") // 以前はこう出来ていたが
query(db, where("..."), limit("...")) // v9ではこういう感じ
```

## typescript

- [withConverter][3]を使う

```ts
import {
  YourCollectionInterface
} from "..."
import { getApp, getApps, initializeApp } from "firebase/app"
import {
  collection,
  FirestoreDataConverter,
  getFirestore,
  QueryDocumentSnapshot,
  WithFieldValue
} from "firebase/firestore"

const firebaseConfig = { ... }
const apps = getApps()
const app = apps.find(({ name }) => name == "client")
  ? getApp("client")
  : initializeApp(firebaseConfig, "client")
const firestore = getFirestore(app)

const converter = <T>(): FirestoreDataConverter<T> => ({
  toFirestore: (data: WithFieldValue<T>) => data,
  fromFirestore: (snapshot: QueryDocumentSnapshot<T>) => {
    const data = snapshot.data()
    Object.keys(data).forEach((key) => {
      if (
        typeof data[key].toString == "function" &&
        data[key].toString().startsWith("Timestamp")
      ) {
        // firestoreのtimestamp型は扱いにくいのでdate型に変更させる
        data[key] = data[key].toDate()
      }
    })

    return data
  },
})

const dataPoint = <T>(collectionPath: string) =>
  collection(firestore, collectionPath).withConverter(converter<T>())

const db = {
  yourCollectionInterface: dataPoint<YourCollectionInterface>("..."),
}


export { firestore, db }
```

```ts:YourCollectionInterface.ts
export interface YourCollectionInterface {
  name: string
  value: number
  createdAt: Date // firestore側がTimestamp型でもconverterで強制的にDateに変えている
}
```

こうすることで、以下のように出来る。

```ts
import { db } from "..."
import {
  getDocs,
  query,
  where,
  orderBy,
} from "firebase/firestore"

const q = query(
  db.yourCollectionName,
  where("..."),
  orderBy("...")
)
const snapshot = await getDocs(q)
const records = snapshot.docs.map((doc) => doc.data())

// ここでコード補完が効く
records[0].name 
records[0].value 
records[0].createdAt // Date型
```


# [server][2]

- サーバーなのでバンドルサイズは気にしなくていいので、v8以前今まで通りのクラスベースのまま。
- クライアントからはアクセスを制限しているデータにアクセスできる。

## typescript

- [withConverter][3]を使う



```ts
import * as admin from "firebase-admin"
import {
  YourCollectionInterface,
} from "."

const firebaseConfig = { ... }


// https://firebase.google.com/docs/admin/setup?hl=ja
// GOOGLE_APPLICATION_CREDENTIALS を使っても良い
// vercelに置く場合などは、GOOGLE_APPLICATION_CREDENTIALSでパスの指定が困難なので直接読み込んだほうが良いと思う
const serviceAccount = require("firebase-key.json")

if (!admin.apps.find(({ name }) => name == "server")) {
  admin.initializeApp(
    {
      credential: admin.credential.cert(serviceAccount),
      databaseURL: firebaseConfig.databaseURL,
    },
    "server"
  )
}
const firestore = admin.app("server").firestore()
const FirebaseFirestore = admin.firestore // FirebaseFirestore.Timestamp.fromDate(...) とかするときに便利


const converter = <T>(): FirebaseFirestore.FirestoreDataConverter<T> => ({
  toFirestore: (data: T) => data,
  fromFirestore: (snapshot: FirebaseFirestore.QueryDocumentSnapshot<T>) => {
    const data = snapshot.data()
    Object.keys(data).forEach((key) => {
      if (typeof data[key].toDate == "function" && typeof data[key].seconds == "number") {
        // Timestamp型は扱いにくいのでDate型に強制変更
        data[key] = data[key].toDate()
      }
    })

    return data
  },
})

const dataPoint = <T>(collectionPath: string) =>
  firestore.collection(collectionPath).withConverter(converter<T>())

const db = {
  yourCollectionInterface: dataPoint<YourCollectionInterface>("..."),
}
export { FirebaseFirestore, db }
```

こうすることで

```ts
const snapshot = await db.yourCollectionInterface
  .where("createdAt", ">", FirebaseFirestore.Timestamp.fromDate(...))
  .orderBy("createdAt", "desc")
  .get()

const records = snapshot.docs
  .map((doc) => doc.data())

// コード補完が効く
records[0].name
records[0].value
records[0].createdAt // Date型
```

# next.js での注意点

- サーバーサイド用の`firebase-admin`はクライアント側のコード（最終的にクライアント側に流されるコード部分）で使わない
- 使ってしまうと、多分`Module not found: Can't resolve 'fs'`のエラーが出る

# next.js getStaticProps、getServerSidePropsでの注意点

```
Reason: `object` ("[object Date]") cannot be serialized as JSON. 
Please only return JSON serializable data types.
```

Timestamp型はもちろん、Date型もpropsとして渡そうとするとJSON serializableじゃないと言ってエラーになる。

- TimestampはDate型にしないとpropsで渡すのは無理そう
- Date型は本来JSON serializableなんだけど、next.jsはエラーになる

## 解決策

https://github.com/vercel/next.js/issues/13209

[superjson][4]を使う。readme通りにやればいい。




[1]: https://firebase.google.com/docs/web/setup
[2]: https://firebase.google.com/docs/admin/setup?hl=ja
[3]: https://firebase.google.com/docs/reference/js/v8/firebase.firestore.FirestoreDataConverter?hl=ja
[4]: https://github.com/blitz-js/superjson#using-with-nextjs