Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
1
Help us understand the problem. What are the problem?

posted at

updated at

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

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通りにやればいい。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
1
Help us understand the problem. What are the problem?