JavaScriptでFirestoreにDateを入れるとTimestampに変換される

React Native Expo に Firestore を導入して開発していたところ、Date を入れた時に Timestamp 型へと変換されてしまうことに気付きました。

Firestore で JavaScript の Date 型を使いたい場合は、WithConverter で Timestamp 型から Date 型に戻してあげる必要があります。

Firebase Timestamp

Firebase のドキュメントを見ると、Timestamp 型はナノ秒の精度があり、Date 型に変換する I/F を持っています。

Timestamp  |  Firebase

Date toDate() Returns a new Date corresponding to this timestamp.

Firestore からの読み込み時に処理を挟む WithConverter

withConverter を使うと、get()の手前にコンバータを挟むことができます。

const tasksSnapshot = await dbh
  .collection("users")
  .doc(userId)
  .collection("tasks")
  .withConverter(Task.converter)
  .get();

次に、コンバータの中身を定義します。Timestamp.toDate()を使って Date 型に変換し、他のプロパティは特に何もせずそのまま return しています。

export const converter = {
  fromFirestore: function (
    snapshot: firebase.firestore.QueryDocumentSnapshot<ModelFromFirestore>,
    options: firebase.firestore.SnapshotOptions
  ): Model {
    const { schedule, ...data } = snapshot.data();
    const date = schedule.toDate();
    return {
      schedule: date,
      ...data,
    };
  },
};

Firestore に入れる前の型と取り出した後の型が変わります。TypeScript の場合は変わる前後両方の方を定義してあげます。

export interface Model {
  id: string;
  name: string;
  schedule: Date;
}

export interface ModelFromFirestore {
  id: string;
  name: string;
  schedule: firebase.firestore.Timestamp;
}

ModelFromFirestore を dry に書く

上記のコードの Model と ModelFromFirestore はほとんど同じなので dry に書きたいところですが、プロパティの型を変更して継承させることはできません。なので、以下のように中継型を作ってあげると良いです。

// 指定したプロパティを上書きして継承するための中継型
export type Weaken<T, K extends keyof T> = {
  [P in keyof T]: P extends K ? any : T[P];
};

export interface ModelFromFirestore extends Util.Weaken<Model, "schedule"> {
  schedule: firebase.firestore.Timestamp;
}

中継型を乱用すると良いこと無さそうですが、今回のような用途であれば OK かと思います。

無事に Timestamp を Date に変換できました。