LoginSignup
14
2

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-10-05
package.json
{
  "firebase": "^9.0.2",
  "firebase-admin": "^9.11.1",
}

client

  • v9ではバンドルサイズを最小限にするために、関数ベースに切り替わっている。
// db.collection("...").where("...") // 以前はこう出来ていたが
query(db, where("..."), limit("...")) // v9ではこういう感じ

typescript

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 }
YourCollectionInterface.ts
export interface YourCollectionInterface {
  name: string
  value: number
  createdAt: Date // firestore側がTimestamp型でもconverterで強制的にDateに変えている
}

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

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

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

typescript

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 }

こうすることで

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はエラーになる

解決策

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

14
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
2